refactor consumers into widgets
This commit is contained in:
+604
-326
@@ -424,6 +424,534 @@ class _BrowserScaffoldTheme extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes auto-hide toolbar visibility for [selectedTabId] and animates
|
||||
/// [child] in/out via [_AnimatedToolbar]. Shared by the bottom, top and side
|
||||
/// rail toolbar layers so the (identical) visibility logic isn't duplicated
|
||||
/// across ad-hoc `Consumer` closures.
|
||||
class _ToolbarVisibilityAnimator extends ConsumerWidget {
|
||||
final TabBarPosition position;
|
||||
final String? selectedTabId;
|
||||
final bool sheetDisplayed;
|
||||
final bool tabInFullScreen;
|
||||
final Widget child;
|
||||
|
||||
const _ToolbarVisibilityAnimator({
|
||||
required this.position,
|
||||
required this.selectedTabId,
|
||||
required this.sheetDisplayed,
|
||||
required this.tabInFullScreen,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen && toolbarState == ToolbarVisibility.visible);
|
||||
|
||||
return _AnimatedToolbar(position: position, visible: visible, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer 0: browser content, positioned around the (possibly auto-hidden)
|
||||
/// toolbars/rail.
|
||||
class _BrowserContentPositioned extends ConsumerWidget {
|
||||
final OverlayPortalController overlayController;
|
||||
final StreamController<Offset> pointerMoveEventsController;
|
||||
final String? selectedTabId;
|
||||
final bool sheetDisplayed;
|
||||
final bool tabInFullScreen;
|
||||
final TabBarPosition tabBarPosition;
|
||||
final bool autoHideTabBar;
|
||||
final bool isRail;
|
||||
final bool isSmallWebActive;
|
||||
final double sideRailTotalWidth;
|
||||
final double topAppBarTotalHeight;
|
||||
final double bottomAppBarTotalHeight;
|
||||
|
||||
const _BrowserContentPositioned({
|
||||
required this.overlayController,
|
||||
required this.pointerMoveEventsController,
|
||||
required this.selectedTabId,
|
||||
required this.sheetDisplayed,
|
||||
required this.tabInFullScreen,
|
||||
required this.tabBarPosition,
|
||||
required this.autoHideTabBar,
|
||||
required this.isRail,
|
||||
required this.isSmallWebActive,
|
||||
required this.sideRailTotalWidth,
|
||||
required this.topAppBarTotalHeight,
|
||||
required this.bottomAppBarTotalHeight,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final toolbarVisible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen && toolbarState == ToolbarVisibility.visible);
|
||||
|
||||
// When auto-hide is disabled, constrain browser above toolbar
|
||||
// (unless toolbar is manually dismissed via swipe gesture).
|
||||
// Gated to horizontal positions so the rail (which forces
|
||||
// auto-hide off) doesn't reserve a phantom bottom inset.
|
||||
final bottomOffset =
|
||||
(tabBarPosition.isHorizontal && !autoHideTabBar && toolbarVisible)
|
||||
? bottomAppBarTotalHeight
|
||||
: 0.0;
|
||||
|
||||
// For top bar: constrain browser below toolbar when visible
|
||||
// to ensure top-of-page content is always accessible
|
||||
final topOffset = (tabBarPosition == TabBarPosition.top && toolbarVisible)
|
||||
? topAppBarTotalHeight
|
||||
: 0.0;
|
||||
|
||||
// Side rail: inset the browser by the rail width on the docked
|
||||
// edge whenever the rail is visible (no auto-hide, so this is
|
||||
// a plain content offset — the rail slides out on dismiss).
|
||||
final leftOffset = (tabBarPosition == TabBarPosition.left && toolbarVisible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0;
|
||||
final rightOffset =
|
||||
(tabBarPosition == TabBarPosition.right && toolbarVisible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0;
|
||||
|
||||
// Only fall back to the system bottom inset when the bottom
|
||||
// toolbar was explicitly dismissed. The normal auto-hide
|
||||
// hidden state is still handled by GeckoView's dynamic
|
||||
// toolbar/clipping logic. On the rail there is never a bottom
|
||||
// bar, so the browser always needs the bottom safe inset.
|
||||
final applyBottomSafeArea = isRail
|
||||
? (!tabInFullScreen && !isSmallWebActive && !sheetDisplayed)
|
||||
: (!tabInFullScreen &&
|
||||
!isSmallWebActive &&
|
||||
!sheetDisplayed &&
|
||||
bottomOffset == 0 &&
|
||||
toolbarState == ToolbarVisibility.dismissed);
|
||||
|
||||
return Positioned(
|
||||
left: leftOffset,
|
||||
right: rightOffset,
|
||||
top: topOffset,
|
||||
bottom: bottomOffset,
|
||||
child: _Browser(
|
||||
overlayController: overlayController,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
pointerMoveEventSink: autoHideTabBar
|
||||
? pointerMoveEventsController.sink
|
||||
: null,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
hasTopBarOffset: topOffset > 0,
|
||||
hasLeftBarOffset: leftOffset > 0,
|
||||
hasRightBarOffset: rightOffset > 0,
|
||||
applyBottomSafeArea: applyBottomSafeArea,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer 2: bottom toolbar (or the small-web discovery overlay in its place).
|
||||
class _BottomToolbarLayer extends StatelessWidget {
|
||||
final bool sheetDisplayed;
|
||||
final bool tabInFullScreen;
|
||||
final bool isSmallWebActive;
|
||||
final TabBarPosition tabBarPosition;
|
||||
final bool showContextualToolbar;
|
||||
final int quickTabSwitcherRowCount;
|
||||
final String? selectedTabId;
|
||||
final StreamController<Offset> pointerMoveEventsController;
|
||||
|
||||
const _BottomToolbarLayer({
|
||||
required this.sheetDisplayed,
|
||||
required this.tabInFullScreen,
|
||||
required this.isSmallWebActive,
|
||||
required this.tabBarPosition,
|
||||
required this.showContextualToolbar,
|
||||
required this.quickTabSwitcherRowCount,
|
||||
required this.selectedTabId,
|
||||
required this.pointerMoveEventsController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isSmallWebActive) {
|
||||
return _AnimatedToolbar(
|
||||
position: TabBarPosition.bottom,
|
||||
visible: !tabInFullScreen,
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
elevation: 3,
|
||||
child: const SmallWebBrowserOverlay(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// _TabBar is passed via `child` so it is built once and reused across
|
||||
// toolbar show/hide toggles inside _ToolbarVisibilityAnimator.
|
||||
return _ToolbarVisibilityAnimator(
|
||||
position: TabBarPosition.bottom,
|
||||
selectedTabId: selectedTabId,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.bottom,
|
||||
showMainToolbar: tabBarPosition == TabBarPosition.bottom,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||
isSmallWebMode: false,
|
||||
pointerMoveEvents: tabBarPosition == TabBarPosition.bottom
|
||||
? pointerMoveEventsController.stream
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer 3: top toolbar (only rendered when the tab bar position is top).
|
||||
class _TopToolbarLayer extends StatelessWidget {
|
||||
final bool sheetDisplayed;
|
||||
final bool tabInFullScreen;
|
||||
final bool isSmallWebActive;
|
||||
final bool showContextualToolbar;
|
||||
final int quickTabSwitcherRowCount;
|
||||
final String? selectedTabId;
|
||||
final StreamController<Offset> pointerMoveEventsController;
|
||||
|
||||
const _TopToolbarLayer({
|
||||
required this.sheetDisplayed,
|
||||
required this.tabInFullScreen,
|
||||
required this.isSmallWebActive,
|
||||
required this.showContextualToolbar,
|
||||
required this.quickTabSwitcherRowCount,
|
||||
required this.selectedTabId,
|
||||
required this.pointerMoveEventsController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _ToolbarVisibilityAnimator(
|
||||
position: TabBarPosition.top,
|
||||
selectedTabId: selectedTabId,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.top,
|
||||
showMainToolbar: true,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||
isSmallWebMode: isSmallWebActive,
|
||||
enableGestures: !isSmallWebActive,
|
||||
pointerMoveEvents: isSmallWebActive
|
||||
? null
|
||||
: pointerMoveEventsController.stream,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer 3b: vertical side rail (left/right).
|
||||
class _SideRailToolbarLayer extends StatelessWidget {
|
||||
final bool sheetDisplayed;
|
||||
final bool tabInFullScreen;
|
||||
final TabBarPosition tabBarPosition;
|
||||
final bool showContextualToolbar;
|
||||
final int quickTabSwitcherRowCount;
|
||||
final String? selectedTabId;
|
||||
|
||||
const _SideRailToolbarLayer({
|
||||
required this.sheetDisplayed,
|
||||
required this.tabInFullScreen,
|
||||
required this.tabBarPosition,
|
||||
required this.showContextualToolbar,
|
||||
required this.quickTabSwitcherRowCount,
|
||||
required this.selectedTabId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _ToolbarVisibilityAnimator(
|
||||
position: tabBarPosition,
|
||||
selectedTabId: selectedTabId,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
child: BrowserSideRail(
|
||||
position: tabBarPosition,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||
isSmallWebMode: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer 4: draggable FAB, positioned clear of the toolbar/rail.
|
||||
class _FabPositioner extends ConsumerWidget {
|
||||
final String? selectedTabId;
|
||||
final bool sheetDisplayed;
|
||||
final bool tabInFullScreen;
|
||||
final TabBarPosition tabBarPosition;
|
||||
final double bottomAppBarTotalHeight;
|
||||
final double bottomSafeArea;
|
||||
final double sideRailTotalWidth;
|
||||
final Widget child;
|
||||
|
||||
const _FabPositioner({
|
||||
required this.selectedTabId,
|
||||
required this.sheetDisplayed,
|
||||
required this.tabInFullScreen,
|
||||
required this.tabBarPosition,
|
||||
required this.bottomAppBarTotalHeight,
|
||||
required this.bottomSafeArea,
|
||||
required this.sideRailTotalWidth,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen && toolbarState == ToolbarVisibility.visible);
|
||||
|
||||
return DraggableFab(
|
||||
bottomToolbarVisible: visible,
|
||||
bottomAppBarHeight: bottomAppBarTotalHeight,
|
||||
bottomSafeArea: bottomSafeArea,
|
||||
leftReservedWidth: (tabBarPosition == TabBarPosition.left && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
rightReservedWidth: (tabBarPosition == TabBarPosition.right && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer 5: page load progress indicator, animated with toolbar visibility.
|
||||
/// Split into a positioner (reacts to toolbar visibility) and a content
|
||||
/// widget (reacts to load progress) so the two independent watches don't
|
||||
/// force each other to rebuild.
|
||||
class _ProgressIndicatorPositioner extends ConsumerWidget {
|
||||
final String? selectedTabId;
|
||||
final bool sheetDisplayed;
|
||||
final bool tabInFullScreen;
|
||||
final TabBarPosition tabBarPosition;
|
||||
final bool isRail;
|
||||
final double sideRailTotalWidth;
|
||||
final double topSafeArea;
|
||||
final double topAppBarTotalHeight;
|
||||
final double bottomAppBarTotalHeight;
|
||||
final Widget child;
|
||||
|
||||
const _ProgressIndicatorPositioner({
|
||||
required this.selectedTabId,
|
||||
required this.sheetDisplayed,
|
||||
required this.tabInFullScreen,
|
||||
required this.tabBarPosition,
|
||||
required this.isRail,
|
||||
required this.sideRailTotalWidth,
|
||||
required this.topSafeArea,
|
||||
required this.topAppBarTotalHeight,
|
||||
required this.bottomAppBarTotalHeight,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final disableAnimations = MediaQuery.disableAnimationsOf(context);
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen && toolbarState == ToolbarVisibility.visible);
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: disableAnimations
|
||||
? Duration.zero
|
||||
: _AnimatedToolbar._kAnimationDuration,
|
||||
curve: Curves.easeInOutQuart,
|
||||
// Rail insets track the rail's visibility so overlays
|
||||
// reclaim the space when it is swiped away.
|
||||
left: (tabBarPosition == TabBarPosition.left && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
right: (tabBarPosition == TabBarPosition.right && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
// On the rail there is no top/bottom bar; pin the progress
|
||||
// line to the top of the content area.
|
||||
top: isRail
|
||||
? topSafeArea
|
||||
: tabBarPosition == TabBarPosition.top && visible
|
||||
? topAppBarTotalHeight
|
||||
: null,
|
||||
bottom: isRail
|
||||
? null
|
||||
: tabBarPosition == TabBarPosition.bottom && visible
|
||||
? bottomAppBarTotalHeight
|
||||
: tabBarPosition == TabBarPosition.bottom
|
||||
? 0
|
||||
: null,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressIndicatorBar extends ConsumerWidget {
|
||||
const _ProgressIndicatorBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final value = ref.watch(
|
||||
selectedTabStateProvider.select((state) {
|
||||
if (state?.isLoading == true) {
|
||||
return state?.progress ?? 100;
|
||||
}
|
||||
|
||||
//When not loading we assumed finished
|
||||
return 100;
|
||||
}),
|
||||
);
|
||||
|
||||
return Visibility(
|
||||
visible: value < 100,
|
||||
child: LinearProgressIndicator(value: value / 100),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer 6: find-in-page widget, positioned above the toolbar/keyboard.
|
||||
/// Same positioner/content split rationale as the progress indicator above.
|
||||
class _FindInPagePositioner extends ConsumerWidget {
|
||||
final String? selectedTabId;
|
||||
final bool sheetDisplayed;
|
||||
final bool tabInFullScreen;
|
||||
final TabBarPosition tabBarPosition;
|
||||
final bool isRail;
|
||||
final double sideRailTotalWidth;
|
||||
final double bottomSafeArea;
|
||||
final double bottomAppBarTotalHeight;
|
||||
final Widget child;
|
||||
|
||||
const _FindInPagePositioner({
|
||||
required this.selectedTabId,
|
||||
required this.sheetDisplayed,
|
||||
required this.tabInFullScreen,
|
||||
required this.tabBarPosition,
|
||||
required this.isRail,
|
||||
required this.sideRailTotalWidth,
|
||||
required this.bottomSafeArea,
|
||||
required this.bottomAppBarTotalHeight,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final disableAnimations = MediaQuery.disableAnimationsOf(context);
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen && toolbarState == ToolbarVisibility.visible);
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: disableAnimations
|
||||
? Duration.zero
|
||||
: _AnimatedToolbar._kAnimationDuration,
|
||||
curve: Curves.easeInOutQuart,
|
||||
left: (tabBarPosition == TabBarPosition.left && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
right: (tabBarPosition == TabBarPosition.right && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
bottom: math.max(
|
||||
isRail
|
||||
? bottomSafeArea
|
||||
: (visible ? bottomAppBarTotalHeight : bottomSafeArea),
|
||||
MediaQuery.viewInsetsOf(context).bottom,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FindInPageContent extends ConsumerWidget {
|
||||
const _FindInPageContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabId = ref.watch(selectedTabProvider);
|
||||
if (tabId == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return FindInPageWidget(key: ValueKey(tabId), tabId: tabId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer 7: app-link prompt banner (§2.6). Anchored above the bottom app
|
||||
/// bar / keyboard exactly like find-in-page, so it is never hidden behind
|
||||
/// the toolbar. Custom Tab sessions are prompted natively instead; this is
|
||||
/// the browser-tab surface only.
|
||||
class _AppLinkPromptLayer extends ConsumerWidget {
|
||||
final String? selectedTabId;
|
||||
final bool sheetDisplayed;
|
||||
final bool tabInFullScreen;
|
||||
final TabBarPosition tabBarPosition;
|
||||
final bool isRail;
|
||||
final double sideRailTotalWidth;
|
||||
final double bottomSafeArea;
|
||||
final double bottomAppBarTotalHeight;
|
||||
|
||||
const _AppLinkPromptLayer({
|
||||
required this.selectedTabId,
|
||||
required this.sheetDisplayed,
|
||||
required this.tabInFullScreen,
|
||||
required this.tabBarPosition,
|
||||
required this.isRail,
|
||||
required this.sideRailTotalWidth,
|
||||
required this.bottomSafeArea,
|
||||
required this.bottomAppBarTotalHeight,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen && toolbarState == ToolbarVisibility.visible);
|
||||
|
||||
return Positioned(
|
||||
left: (tabBarPosition == TabBarPosition.left && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
right: (tabBarPosition == TabBarPosition.right && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
bottom: math.max(
|
||||
isRail
|
||||
? bottomSafeArea
|
||||
: (visible ? bottomAppBarTotalHeight : bottomSafeArea),
|
||||
MediaQuery.viewInsetsOf(context).bottom,
|
||||
),
|
||||
child: const AppLinkPromptHost(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserScreen extends HookConsumerWidget {
|
||||
const BrowserScreen({super.key});
|
||||
|
||||
@@ -908,80 +1436,19 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
// Layer 0: Browser content
|
||||
// Position changes instantly (no animation) to avoid jarring native view resize
|
||||
// The toolbar itself animates, providing visual continuity
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final toolbarVisible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen &&
|
||||
toolbarState == ToolbarVisibility.visible);
|
||||
|
||||
// When auto-hide is disabled, constrain browser above toolbar
|
||||
// (unless toolbar is manually dismissed via swipe gesture).
|
||||
// Gated to horizontal positions so the rail (which forces
|
||||
// auto-hide off) doesn't reserve a phantom bottom inset.
|
||||
final bottomOffset =
|
||||
(tabBarPosition.isHorizontal &&
|
||||
!autoHideTabBar &&
|
||||
toolbarVisible)
|
||||
? bottomAppBarTotalHeight
|
||||
: 0.0;
|
||||
|
||||
// For top bar: constrain browser below toolbar when visible
|
||||
// to ensure top-of-page content is always accessible
|
||||
final topOffset =
|
||||
(tabBarPosition == TabBarPosition.top && toolbarVisible)
|
||||
? topAppBarTotalHeight
|
||||
: 0.0;
|
||||
|
||||
// Side rail: inset the browser by the rail width on the docked
|
||||
// edge whenever the rail is visible (no auto-hide, so this is
|
||||
// a plain content offset — the rail slides out on dismiss).
|
||||
final leftOffset =
|
||||
(tabBarPosition == TabBarPosition.left && toolbarVisible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0;
|
||||
final rightOffset =
|
||||
(tabBarPosition == TabBarPosition.right && toolbarVisible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0;
|
||||
|
||||
// Only fall back to the system bottom inset when the bottom
|
||||
// toolbar was explicitly dismissed. The normal auto-hide
|
||||
// hidden state is still handled by GeckoView's dynamic
|
||||
// toolbar/clipping logic. On the rail there is never a bottom
|
||||
// bar, so the browser always needs the bottom safe inset.
|
||||
final applyBottomSafeArea = isRail
|
||||
? (!tabInFullScreen &&
|
||||
!isSmallWebActive &&
|
||||
!sheetDisplayed)
|
||||
: (!tabInFullScreen &&
|
||||
!isSmallWebActive &&
|
||||
!sheetDisplayed &&
|
||||
bottomOffset == 0 &&
|
||||
toolbarState == ToolbarVisibility.dismissed);
|
||||
|
||||
return Positioned(
|
||||
left: leftOffset,
|
||||
right: rightOffset,
|
||||
top: topOffset,
|
||||
bottom: bottomOffset,
|
||||
child: _Browser(
|
||||
overlayController: overlayController,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
pointerMoveEventSink: autoHideTabBar
|
||||
? pointerMoveEventsController.sink
|
||||
: null,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
hasTopBarOffset: topOffset > 0,
|
||||
hasLeftBarOffset: leftOffset > 0,
|
||||
hasRightBarOffset: rightOffset > 0,
|
||||
applyBottomSafeArea: applyBottomSafeArea,
|
||||
),
|
||||
);
|
||||
},
|
||||
_BrowserContentPositioned(
|
||||
overlayController: overlayController,
|
||||
pointerMoveEventsController: pointerMoveEventsController,
|
||||
selectedTabId: selectedTabId,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
tabBarPosition: tabBarPosition,
|
||||
autoHideTabBar: autoHideTabBar,
|
||||
isRail: isRail,
|
||||
isSmallWebActive: isSmallWebActive,
|
||||
sideRailTotalWidth: sideRailTotalWidth,
|
||||
topAppBarTotalHeight: topAppBarTotalHeight,
|
||||
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||
),
|
||||
|
||||
// Layer 0.5: System bar tint — fills the status-bar/nav-bar
|
||||
@@ -1021,52 +1488,16 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: isSmallWebActive
|
||||
? _AnimatedToolbar(
|
||||
position: TabBarPosition.bottom,
|
||||
visible: !tabInFullScreen,
|
||||
child: Material(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
elevation: 3,
|
||||
child: const SmallWebBrowserOverlay(),
|
||||
),
|
||||
)
|
||||
: Consumer(
|
||||
// _TabBar is passed via `child` so it is built once and
|
||||
// reused across toolbar show/hide toggles; only the
|
||||
// `visible` flag fed to _AnimatedToolbar depends on the
|
||||
// watched provider.
|
||||
builder: (context, ref, child) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(
|
||||
selectedTabId,
|
||||
),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen &&
|
||||
toolbarState == ToolbarVisibility.visible);
|
||||
return _AnimatedToolbar(
|
||||
position: TabBarPosition.bottom,
|
||||
visible: visible,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.bottom,
|
||||
showMainToolbar:
|
||||
tabBarPosition == TabBarPosition.bottom,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||
isSmallWebMode: false,
|
||||
pointerMoveEvents:
|
||||
tabBarPosition == TabBarPosition.bottom
|
||||
? pointerMoveEventsController.stream
|
||||
: null,
|
||||
),
|
||||
),
|
||||
child: _BottomToolbarLayer(
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
isSmallWebActive: isSmallWebActive,
|
||||
tabBarPosition: tabBarPosition,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||
selectedTabId: selectedTabId,
|
||||
pointerMoveEventsController: pointerMoveEventsController,
|
||||
),
|
||||
),
|
||||
|
||||
// Layer 3: Top Toolbar (overlay, slides in/out) - only when position is top
|
||||
@@ -1075,36 +1506,14 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Consumer(
|
||||
// _TabBar is passed via `child` so it is built once and
|
||||
// reused across toolbar show/hide toggles; only the
|
||||
// `visible` flag fed to _AnimatedToolbar depends on the
|
||||
// watched provider.
|
||||
builder: (context, ref, child) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen &&
|
||||
toolbarState == ToolbarVisibility.visible);
|
||||
return _AnimatedToolbar(
|
||||
position: TabBarPosition.top,
|
||||
visible: visible,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.top,
|
||||
showMainToolbar: true,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||
isSmallWebMode: isSmallWebActive,
|
||||
enableGestures: !isSmallWebActive,
|
||||
pointerMoveEvents: isSmallWebActive
|
||||
? null
|
||||
: pointerMoveEventsController.stream,
|
||||
),
|
||||
child: _TopToolbarLayer(
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
isSmallWebActive: isSmallWebActive,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||
selectedTabId: selectedTabId,
|
||||
pointerMoveEventsController: pointerMoveEventsController,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1116,199 +1525,68 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
bottom: 0,
|
||||
left: tabBarPosition == TabBarPosition.left ? 0 : null,
|
||||
right: tabBarPosition == TabBarPosition.right ? 0 : null,
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen &&
|
||||
toolbarState == ToolbarVisibility.visible);
|
||||
return _AnimatedToolbar(
|
||||
position: tabBarPosition,
|
||||
visible: visible,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: BrowserSideRail(
|
||||
position: tabBarPosition,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||
isSmallWebMode: false,
|
||||
),
|
||||
child: _SideRailToolbarLayer(
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
tabBarPosition: tabBarPosition,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||
selectedTabId: selectedTabId,
|
||||
),
|
||||
),
|
||||
|
||||
// Layer 4: FAB (draggable via long press)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen &&
|
||||
toolbarState == ToolbarVisibility.visible);
|
||||
return DraggableFab(
|
||||
bottomToolbarVisible: visible,
|
||||
bottomAppBarHeight: bottomAppBarTotalHeight,
|
||||
bottomSafeArea: bottomSafeArea,
|
||||
leftReservedWidth:
|
||||
(tabBarPosition == TabBarPosition.left && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
rightReservedWidth:
|
||||
(tabBarPosition == TabBarPosition.right && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
_FabPositioner(
|
||||
selectedTabId: selectedTabId,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
tabBarPosition: tabBarPosition,
|
||||
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||
bottomSafeArea: bottomSafeArea,
|
||||
sideRailTotalWidth: sideRailTotalWidth,
|
||||
child: const BrowserFab(),
|
||||
),
|
||||
|
||||
// Layer 5: Page load progress indicator (animates with toolbar visibility)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final disableAnimations = MediaQuery.disableAnimationsOf(
|
||||
context,
|
||||
);
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen &&
|
||||
toolbarState == ToolbarVisibility.visible);
|
||||
return AnimatedPositioned(
|
||||
duration: disableAnimations
|
||||
? Duration.zero
|
||||
: _AnimatedToolbar._kAnimationDuration,
|
||||
curve: Curves.easeInOutQuart,
|
||||
// Rail insets track the rail's visibility so overlays
|
||||
// reclaim the space when it is swiped away.
|
||||
left: (tabBarPosition == TabBarPosition.left && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
right: (tabBarPosition == TabBarPosition.right && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
// On the rail there is no top/bottom bar; pin the progress
|
||||
// line to the top of the content area.
|
||||
top: isRail
|
||||
? topSafeArea
|
||||
: tabBarPosition == TabBarPosition.top && visible
|
||||
? topAppBarTotalHeight
|
||||
: null,
|
||||
bottom: isRail
|
||||
? null
|
||||
: tabBarPosition == TabBarPosition.bottom && visible
|
||||
? bottomAppBarTotalHeight
|
||||
: tabBarPosition == TabBarPosition.bottom
|
||||
? 0
|
||||
: null,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final value = ref.watch(
|
||||
selectedTabStateProvider.select((state) {
|
||||
if (state?.isLoading == true) {
|
||||
return state?.progress ?? 100;
|
||||
}
|
||||
|
||||
//When not loading we assumed finished
|
||||
return 100;
|
||||
}),
|
||||
);
|
||||
|
||||
return Visibility(
|
||||
visible: value < 100,
|
||||
child: LinearProgressIndicator(value: value / 100),
|
||||
);
|
||||
},
|
||||
),
|
||||
_ProgressIndicatorPositioner(
|
||||
selectedTabId: selectedTabId,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
tabBarPosition: tabBarPosition,
|
||||
isRail: isRail,
|
||||
sideRailTotalWidth: sideRailTotalWidth,
|
||||
topSafeArea: topSafeArea,
|
||||
topAppBarTotalHeight: topAppBarTotalHeight,
|
||||
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||
child: const _ProgressIndicatorBar(),
|
||||
),
|
||||
|
||||
// Layer 6: Find in Page widget (above toolbar or keyboard, whichever is higher)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final disableAnimations = MediaQuery.disableAnimationsOf(
|
||||
context,
|
||||
);
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen &&
|
||||
toolbarState == ToolbarVisibility.visible);
|
||||
return AnimatedPositioned(
|
||||
duration: disableAnimations
|
||||
? Duration.zero
|
||||
: _AnimatedToolbar._kAnimationDuration,
|
||||
curve: Curves.easeInOutQuart,
|
||||
left: (tabBarPosition == TabBarPosition.left && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
right: (tabBarPosition == TabBarPosition.right && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
bottom: math.max(
|
||||
isRail
|
||||
? bottomSafeArea
|
||||
: (visible
|
||||
? bottomAppBarTotalHeight
|
||||
: bottomSafeArea),
|
||||
MediaQuery.viewInsetsOf(context).bottom,
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabId = ref.watch(selectedTabProvider);
|
||||
if (tabId == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return FindInPageWidget(key: ValueKey(tabId), tabId: tabId);
|
||||
},
|
||||
),
|
||||
_FindInPagePositioner(
|
||||
selectedTabId: selectedTabId,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
tabBarPosition: tabBarPosition,
|
||||
isRail: isRail,
|
||||
sideRailTotalWidth: sideRailTotalWidth,
|
||||
bottomSafeArea: bottomSafeArea,
|
||||
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||
child: const _FindInPageContent(),
|
||||
),
|
||||
|
||||
// Layer 7: App-link prompt banner (§2.6). Anchored above the bottom app
|
||||
// bar / keyboard exactly like find-in-page, so it is never hidden behind
|
||||
// the toolbar. Custom Tab sessions are prompted natively instead; this is
|
||||
// the browser-tab surface only.
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
final visible =
|
||||
sheetDisplayed ||
|
||||
(!tabInFullScreen &&
|
||||
toolbarState == ToolbarVisibility.visible);
|
||||
return Positioned(
|
||||
left: (tabBarPosition == TabBarPosition.left && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
right: (tabBarPosition == TabBarPosition.right && visible)
|
||||
? sideRailTotalWidth
|
||||
: 0.0,
|
||||
bottom: math.max(
|
||||
isRail
|
||||
? bottomSafeArea
|
||||
: (visible
|
||||
? bottomAppBarTotalHeight
|
||||
: bottomSafeArea),
|
||||
MediaQuery.viewInsetsOf(context).bottom,
|
||||
),
|
||||
child: const AppLinkPromptHost(),
|
||||
);
|
||||
},
|
||||
_AppLinkPromptLayer(
|
||||
selectedTabId: selectedTabId,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
tabBarPosition: tabBarPosition,
|
||||
isRail: isRail,
|
||||
sideRailTotalWidth: sideRailTotalWidth,
|
||||
bottomSafeArea: bottomSafeArea,
|
||||
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+79
-112
@@ -66,9 +66,9 @@ import 'package:weblibre/features/geckoview/features/readerview/presentation/con
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/entities/container_selection_result.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_relation_visibility.dart';
|
||||
import 'package:weblibre/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart';
|
||||
import 'package:weblibre/features/gestures/data/models/gesture_settings.dart';
|
||||
import 'package:weblibre/features/gestures/domain/repositories/gesture_settings.dart';
|
||||
@@ -1271,134 +1271,101 @@ class _ContainerExpansion extends ConsumerWidget {
|
||||
),
|
||||
|
||||
// URL relation (conditional)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final isSiteAssigned = ref.watch(
|
||||
watchIsCurrentSiteAssignedToContainerProvider,
|
||||
);
|
||||
ContainerRelationUnassignedVisibility(
|
||||
child: _buildSubTile(
|
||||
'Assign URL to Container',
|
||||
icon: MdiIcons.webPlus,
|
||||
onTap: () async {
|
||||
final selection = await const ContainerSelectionRoute()
|
||||
.push<ContainerSelectionResult?>(context);
|
||||
|
||||
if (!isSiteAssigned.hasValue || isSiteAssigned.requireValue) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (selection case ContainerSelectionSelected(
|
||||
:final containerId,
|
||||
)) {
|
||||
final containerData = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(containerId);
|
||||
|
||||
return _buildSubTile(
|
||||
'Assign URL to Container',
|
||||
icon: MdiIcons.webPlus,
|
||||
onTap: () async {
|
||||
final selection = await const ContainerSelectionRoute()
|
||||
.push<ContainerSelectionResult?>(context);
|
||||
if (containerData != null) {
|
||||
final tabState = ref.read(tabStateProvider(selectedTabId));
|
||||
final origin = tabState?.url.origin.mapNotNull(Uri.parse);
|
||||
|
||||
if (selection case ContainerSelectionSelected(
|
||||
:final containerId,
|
||||
)) {
|
||||
if (origin != null) {
|
||||
await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.replaceContainer(
|
||||
containerData.copyWith.metadata(
|
||||
containerData.metadata.copyWith.assignedSites([
|
||||
...?containerData.metadata.assignedSites,
|
||||
origin,
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Unassign URL relation (conditional)
|
||||
ContainerRelationAssignedVisibility(
|
||||
child: _buildSubTile(
|
||||
'Unassign URL from Container',
|
||||
icon: MdiIcons.webMinus,
|
||||
onTap: () async {
|
||||
final tabState = ref.read(tabStateProvider(selectedTabId));
|
||||
final origin = tabState?.url.origin.mapNotNull(Uri.parse);
|
||||
|
||||
if (origin != null) {
|
||||
final containerId = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.siteAssignedContainerId(origin);
|
||||
|
||||
if (containerId != null) {
|
||||
final containerData = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(containerId);
|
||||
|
||||
if (containerData != null) {
|
||||
final tabState = ref.read(
|
||||
tabStateProvider(selectedTabId),
|
||||
);
|
||||
final origin = tabState?.url.origin.mapNotNull(Uri.parse);
|
||||
final updatedSites = containerData.metadata.assignedSites
|
||||
?.where((site) => site != origin)
|
||||
.toList();
|
||||
|
||||
if (origin != null) {
|
||||
await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.replaceContainer(
|
||||
containerData.copyWith.metadata(
|
||||
containerData.metadata.copyWith.assignedSites([
|
||||
...?containerData.metadata.assignedSites,
|
||||
origin,
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Unassign URL relation (conditional)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final isSiteAssigned = ref.watch(
|
||||
watchIsCurrentSiteAssignedToContainerProvider,
|
||||
);
|
||||
|
||||
if (!isSiteAssigned.hasValue || !isSiteAssigned.requireValue) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return _buildSubTile(
|
||||
'Unassign URL from Container',
|
||||
icon: MdiIcons.webMinus,
|
||||
onTap: () async {
|
||||
final tabState = ref.read(tabStateProvider(selectedTabId));
|
||||
final origin = tabState?.url.origin.mapNotNull(Uri.parse);
|
||||
|
||||
if (origin != null) {
|
||||
final containerId = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.siteAssignedContainerId(origin);
|
||||
|
||||
if (containerId != null) {
|
||||
final containerData = await ref
|
||||
await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(containerId);
|
||||
|
||||
if (containerData != null) {
|
||||
final updatedSites = containerData
|
||||
.metadata
|
||||
.assignedSites
|
||||
?.where((site) => site != origin)
|
||||
.toList();
|
||||
|
||||
await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.replaceContainer(
|
||||
containerData.copyWith.metadata(
|
||||
containerData.metadata.copyWith.assignedSites(
|
||||
updatedSites,
|
||||
),
|
||||
.replaceContainer(
|
||||
containerData.copyWith.metadata(
|
||||
containerData.metadata.copyWith.assignedSites(
|
||||
updatedSites,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Unassign Container (conditional)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final containerId = ref.watch(
|
||||
watchContainerTabIdProvider(
|
||||
selectedTabId,
|
||||
).select((value) => value.value),
|
||||
);
|
||||
|
||||
if (containerId == null) return const SizedBox.shrink();
|
||||
|
||||
return _buildSubTile(
|
||||
'Unassign Container',
|
||||
icon: MdiIcons.folderCancelOutline,
|
||||
onTap: () async {
|
||||
final tabState = ref.read(tabStateProvider(selectedTabId))!;
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.unassignContainer(tabState.id);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
ContainerAssignedVisibility(
|
||||
tabId: selectedTabId,
|
||||
child: _buildSubTile(
|
||||
'Unassign Container',
|
||||
icon: MdiIcons.folderCancelOutline,
|
||||
onTap: () async {
|
||||
final tabState = ref.read(tabStateProvider(selectedTabId))!;
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.unassignContainer(tabState.id);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+268
-242
@@ -52,6 +52,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/entities/contai
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_relation_visibility.dart';
|
||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/features/web_search/domain/controllers/sandbox_capture_controller.dart';
|
||||
@@ -155,32 +156,16 @@ class TabMenu extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
if (enableDesktopMode)
|
||||
Consumer(
|
||||
builder: (context, childRef, child) {
|
||||
final enabled = childRef.watch(
|
||||
desktopModeProvider(selectedTabId),
|
||||
);
|
||||
|
||||
return MenuItemButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(desktopModeProvider(selectedTabId).notifier)
|
||||
.toggle();
|
||||
},
|
||||
leadingIcon: const Icon(MdiIcons.monitor),
|
||||
trailingIcon: Checkbox(
|
||||
value: enabled,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(desktopModeProvider(selectedTabId).notifier)
|
||||
.enabled(value);
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
),
|
||||
child: const Text('Desktop Mode'),
|
||||
);
|
||||
_DesktopModeMenuItem(
|
||||
selectedTabId: selectedTabId,
|
||||
controller: controller,
|
||||
onToggle: () {
|
||||
ref.read(desktopModeProvider(selectedTabId).notifier).toggle();
|
||||
},
|
||||
onEnabledChanged: (value) {
|
||||
ref
|
||||
.read(desktopModeProvider(selectedTabId).notifier)
|
||||
.enabled(value);
|
||||
},
|
||||
),
|
||||
if (enableFindInPage || enableReaderMode || enableDesktopMode)
|
||||
@@ -220,35 +205,7 @@ class TabMenu extends HookConsumerWidget {
|
||||
).push(context);
|
||||
},
|
||||
),
|
||||
if (enableAddToHomeScreen)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final isInstallable = ref.watch(isCurrentTabInstallableProvider);
|
||||
final isShortcutable = ref.watch(
|
||||
isCurrentTabShortcutableProvider,
|
||||
);
|
||||
|
||||
return Visibility(
|
||||
visible: isInstallable || isShortcutable,
|
||||
child: MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
leadingIcon: const Icon(Icons.add_to_home_screen),
|
||||
child: const Text('Add to Home Screen'),
|
||||
onPressed: () async {
|
||||
if (isInstallable) {
|
||||
await showPwaInstallDialog(context, ref);
|
||||
} else {
|
||||
await showShortcutInstallDialog(context, ref);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
MenuController.maybeOf(context)?.close();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (enableAddToHomeScreen) const _AddToHomeScreenMenuItem(),
|
||||
if (enableCloneTab)
|
||||
SubmenuButton(
|
||||
menuChildren: [
|
||||
@@ -434,7 +391,7 @@ class TabMenu extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
Consumer(
|
||||
ContainerRelationUnassignedVisibility(
|
||||
child: MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.webPlus),
|
||||
child: const Text('URL relation'),
|
||||
@@ -475,19 +432,8 @@ class TabMenu extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
builder: (context, ref, child) {
|
||||
final isSiteAssigned = ref.watch(
|
||||
watchIsCurrentSiteAssignedToContainerProvider,
|
||||
);
|
||||
|
||||
return Visibility(
|
||||
visible:
|
||||
isSiteAssigned.hasValue && !isSiteAssigned.requireValue,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
),
|
||||
Consumer(
|
||||
ContainerRelationAssignedVisibility(
|
||||
child: MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.webMinus),
|
||||
child: const Text('Unassign URL relation'),
|
||||
@@ -526,19 +472,9 @@ class TabMenu extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
builder: (context, ref, child) {
|
||||
final isSiteAssigned = ref.watch(
|
||||
watchIsCurrentSiteAssignedToContainerProvider,
|
||||
);
|
||||
|
||||
return Visibility(
|
||||
visible:
|
||||
isSiteAssigned.hasValue && isSiteAssigned.requireValue,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
),
|
||||
Consumer(
|
||||
ContainerAssignedVisibility(
|
||||
tabId: selectedTabId,
|
||||
child: MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.folderCancelOutline),
|
||||
child: const Text('Unassign Container'),
|
||||
@@ -550,75 +486,27 @@ class TabMenu extends HookConsumerWidget {
|
||||
.unassignContainer(tabState.id);
|
||||
},
|
||||
),
|
||||
builder: (context, ref, child) {
|
||||
final containerId = ref.watch(
|
||||
watchContainerTabIdProvider(
|
||||
selectedTabId,
|
||||
).select((value) => value.value),
|
||||
);
|
||||
|
||||
return Visibility(
|
||||
visible: containerId != null,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
leadingIcon: const Icon(MdiIcons.folder),
|
||||
child: const Text('Container'),
|
||||
),
|
||||
if (enableHierarchy)
|
||||
Consumer(
|
||||
builder: (childContext, childRef, child) {
|
||||
// `MenuItemButton.onPressed` is dispatched as a post-frame
|
||||
// callback by Flutter's menu_anchor — by the time it fires,
|
||||
// this `Consumer` element (and any context/ref captured from
|
||||
// its builder params) has been deactivated as the menu
|
||||
// overlay tears down. So:
|
||||
// - use `childContext` / `childRef` only synchronously
|
||||
// inside this builder (the `watch` below),
|
||||
// - inside `onPressed`, use the outer `context` and `ref`
|
||||
// from TabMenu.build, which live above the menu overlay
|
||||
// and stay mounted with the trigger button.
|
||||
final movingTab = childRef.watch(
|
||||
watchTabDbDataProvider(selectedTabId),
|
||||
);
|
||||
final tabData = movingTab.value;
|
||||
final hasParent = tabData?.parentId != null;
|
||||
|
||||
final repo = ref.read(tabDataRepositoryProvider.notifier);
|
||||
|
||||
return SubmenuButton(
|
||||
leadingIcon: const Icon(MdiIcons.fileTree),
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.swapHorizontal),
|
||||
onPressed: () async {
|
||||
controller.close();
|
||||
await showTabParentPicker(
|
||||
context: context,
|
||||
ref: ref,
|
||||
tabId: selectedTabId,
|
||||
);
|
||||
},
|
||||
child: const Text('Change parent…'),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.fileTreeOutline),
|
||||
onPressed: hasParent
|
||||
? () async {
|
||||
await repo.setTabParent(
|
||||
tabId: selectedTabId,
|
||||
newParentId: null,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Detach from parent'),
|
||||
),
|
||||
],
|
||||
child: const Text('Hierarchy'),
|
||||
_TabHierarchySubmenu(
|
||||
selectedTabId: selectedTabId,
|
||||
controller: controller,
|
||||
onChangeParent: () async {
|
||||
await showTabParentPicker(
|
||||
context: context,
|
||||
ref: ref,
|
||||
tabId: selectedTabId,
|
||||
);
|
||||
},
|
||||
onDetachFromParent: () async {
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.setTabParent(tabId: selectedTabId, newParentId: null);
|
||||
},
|
||||
),
|
||||
if (enableReorder)
|
||||
SubmenuButton(
|
||||
@@ -708,73 +596,11 @@ class TabMenu extends HookConsumerWidget {
|
||||
leadingIcon: const Icon(MdiIcons.fileExport),
|
||||
child: const Text('Export'),
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final engineState = ref.watch(translationEngineStateProvider);
|
||||
final translationState = ref.watch(
|
||||
tabStateProvider(
|
||||
selectedTabId,
|
||||
).select((s) => s?.translationState),
|
||||
);
|
||||
final readerActive = ref.watch(
|
||||
tabStateProvider(
|
||||
selectedTabId,
|
||||
).select((s) => s?.readerableState.active ?? false),
|
||||
);
|
||||
|
||||
// Hide when reader mode is active (Fenix-aligned)
|
||||
if (readerActive || engineState?.isEngineSupported != true) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isTranslated = translationState?.isTranslated ?? false;
|
||||
|
||||
return MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
leadingIcon: Icon(
|
||||
Icons.translate,
|
||||
color: isTranslated
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
onPressed: () async {
|
||||
controller.close();
|
||||
if (context.mounted) {
|
||||
await showTranslationBottomSheet(
|
||||
context,
|
||||
selectedTabId: selectedTabId,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(isTranslated ? 'Translated' : 'Translate Page'),
|
||||
);
|
||||
},
|
||||
_TranslatePageMenuItem(
|
||||
selectedTabId: selectedTabId,
|
||||
controller: controller,
|
||||
),
|
||||
if (enablePinTab)
|
||||
Consumer(
|
||||
builder: (context, childRef, child) {
|
||||
final isPinned = childRef.watch(
|
||||
watchPinnedTabIdsProvider.select(
|
||||
(v) => v.value?.contains(selectedTabId) ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
return MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
onPressed: () async {
|
||||
await childRef
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.setPinned(selectedTabId, pinned: !isPinned);
|
||||
|
||||
if (context.mounted) {
|
||||
MenuController.maybeOf(context)?.close();
|
||||
}
|
||||
},
|
||||
leadingIcon: Icon(isPinned ? MdiIcons.pinOff : MdiIcons.pin),
|
||||
child: Text(isPinned ? 'Unpin tab' : 'Pin tab'),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (enablePinTab) _PinTabMenuItem(selectedTabId: selectedTabId),
|
||||
if (enableCloseTab)
|
||||
MenuItemButton(
|
||||
onPressed: () =>
|
||||
@@ -797,43 +623,243 @@ class TabMenu extends HookConsumerWidget {
|
||||
child: const Text('Reload'),
|
||||
),
|
||||
if (enableNavigationButtons)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final history = ref.watch(
|
||||
tabStateProvider(
|
||||
selectedTabId,
|
||||
).select((value) => value?.historyState),
|
||||
);
|
||||
|
||||
final isLoading = ref.watch(
|
||||
selectedTabStateProvider.select(
|
||||
(state) => state?.isLoading ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: NavigateBackButton(
|
||||
selectedTabId: selectedTabId,
|
||||
isLoading: isLoading,
|
||||
menuControllerToClose: controller,
|
||||
canGoBack: history?.canGoBack == true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48, child: VerticalDivider()),
|
||||
Expanded(
|
||||
child: NavigateForwardButton(
|
||||
selectedTabId: selectedTabId,
|
||||
menuControllerToClose: controller,
|
||||
canGoForward: history?.canGoForward == true,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
_NavigationButtonsRow(
|
||||
selectedTabId: selectedTabId,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopModeMenuItem extends ConsumerWidget {
|
||||
final String selectedTabId;
|
||||
final MenuController controller;
|
||||
final VoidCallback onToggle;
|
||||
final ValueChanged<bool> onEnabledChanged;
|
||||
|
||||
const _DesktopModeMenuItem({
|
||||
required this.selectedTabId,
|
||||
required this.controller,
|
||||
required this.onToggle,
|
||||
required this.onEnabledChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final enabled = ref.watch(desktopModeProvider(selectedTabId));
|
||||
|
||||
return MenuItemButton(
|
||||
onPressed: onToggle,
|
||||
leadingIcon: const Icon(MdiIcons.monitor),
|
||||
trailingIcon: Checkbox(
|
||||
value: enabled,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
onEnabledChanged(value);
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
),
|
||||
child: const Text('Desktop Mode'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddToHomeScreenMenuItem extends ConsumerWidget {
|
||||
const _AddToHomeScreenMenuItem();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isInstallable = ref.watch(isCurrentTabInstallableProvider);
|
||||
final isShortcutable = ref.watch(isCurrentTabShortcutableProvider);
|
||||
|
||||
return Visibility(
|
||||
visible: isInstallable || isShortcutable,
|
||||
child: MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
leadingIcon: const Icon(Icons.add_to_home_screen),
|
||||
child: const Text('Add to Home Screen'),
|
||||
onPressed: () async {
|
||||
if (isInstallable) {
|
||||
await showPwaInstallDialog(context, ref);
|
||||
} else {
|
||||
await showShortcutInstallDialog(context, ref);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
MenuController.maybeOf(context)?.close();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `MenuItemButton.onPressed` is dispatched as a post-frame callback by
|
||||
/// Flutter's menu_anchor — by the time it fires, this widget (and any
|
||||
/// context/ref it could watch) has been deactivated as the menu overlay
|
||||
/// tears down. So the actual navigation/mutation ([onChangeParent] /
|
||||
/// [onDetachFromParent]) is passed in from TabMenu.build, closing over
|
||||
/// TabMenu's own context/ref, which live above the menu overlay and stay
|
||||
/// mounted with the trigger button.
|
||||
class _TabHierarchySubmenu extends ConsumerWidget {
|
||||
final String selectedTabId;
|
||||
final MenuController controller;
|
||||
final VoidCallback onChangeParent;
|
||||
final VoidCallback onDetachFromParent;
|
||||
|
||||
const _TabHierarchySubmenu({
|
||||
required this.selectedTabId,
|
||||
required this.controller,
|
||||
required this.onChangeParent,
|
||||
required this.onDetachFromParent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final movingTab = ref.watch(watchTabDbDataProvider(selectedTabId));
|
||||
final tabData = movingTab.value;
|
||||
final hasParent = tabData?.parentId != null;
|
||||
|
||||
return SubmenuButton(
|
||||
leadingIcon: const Icon(MdiIcons.fileTree),
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.swapHorizontal),
|
||||
onPressed: () {
|
||||
controller.close();
|
||||
onChangeParent();
|
||||
},
|
||||
child: const Text('Change parent…'),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.fileTreeOutline),
|
||||
onPressed: hasParent ? onDetachFromParent : null,
|
||||
child: const Text('Detach from parent'),
|
||||
),
|
||||
],
|
||||
child: const Text('Hierarchy'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TranslatePageMenuItem extends ConsumerWidget {
|
||||
final String selectedTabId;
|
||||
final MenuController controller;
|
||||
|
||||
const _TranslatePageMenuItem({
|
||||
required this.selectedTabId,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final engineState = ref.watch(translationEngineStateProvider);
|
||||
final translationState = ref.watch(
|
||||
tabStateProvider(selectedTabId).select((s) => s?.translationState),
|
||||
);
|
||||
final readerActive = ref.watch(
|
||||
tabStateProvider(
|
||||
selectedTabId,
|
||||
).select((s) => s?.readerableState.active ?? false),
|
||||
);
|
||||
|
||||
// Hide when reader mode is active (Fenix-aligned)
|
||||
if (readerActive || engineState?.isEngineSupported != true) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isTranslated = translationState?.isTranslated ?? false;
|
||||
|
||||
return MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
leadingIcon: Icon(
|
||||
Icons.translate,
|
||||
color: isTranslated ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
onPressed: () async {
|
||||
controller.close();
|
||||
if (context.mounted) {
|
||||
await showTranslationBottomSheet(
|
||||
context,
|
||||
selectedTabId: selectedTabId,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(isTranslated ? 'Translated' : 'Translate Page'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PinTabMenuItem extends ConsumerWidget {
|
||||
final String selectedTabId;
|
||||
|
||||
const _PinTabMenuItem({required this.selectedTabId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isPinned = ref.watch(
|
||||
watchPinnedTabIdsProvider.select(
|
||||
(v) => v.value?.contains(selectedTabId) ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
return MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.setPinned(selectedTabId, pinned: !isPinned);
|
||||
|
||||
if (context.mounted) {
|
||||
MenuController.maybeOf(context)?.close();
|
||||
}
|
||||
},
|
||||
leadingIcon: Icon(isPinned ? MdiIcons.pinOff : MdiIcons.pin),
|
||||
child: Text(isPinned ? 'Unpin tab' : 'Pin tab'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationButtonsRow extends ConsumerWidget {
|
||||
final String selectedTabId;
|
||||
final MenuController controller;
|
||||
|
||||
const _NavigationButtonsRow({
|
||||
required this.selectedTabId,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final history = ref.watch(
|
||||
tabStateProvider(selectedTabId).select((value) => value?.historyState),
|
||||
);
|
||||
|
||||
final isLoading = ref.watch(
|
||||
selectedTabStateProvider.select((state) => state?.isLoading ?? false),
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: NavigateBackButton(
|
||||
selectedTabId: selectedTabId,
|
||||
isLoading: isLoading,
|
||||
menuControllerToClose: controller,
|
||||
canGoBack: history?.canGoBack == true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48, child: VerticalDivider()),
|
||||
Expanded(
|
||||
child: NavigateForwardButton(
|
||||
selectedTabId: selectedTabId,
|
||||
menuControllerToClose: controller,
|
||||
canGoForward: history?.canGoForward == true,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
|
||||
/// Shows [child] only once [watchIsCurrentSiteAssignedToContainerProvider]
|
||||
/// has resolved and the current tab's site is not yet assigned to a
|
||||
/// container. Shared between TabMenu and the browser menu bottom sheet so
|
||||
/// the "assign URL to container" affordance stays in sync in both surfaces.
|
||||
class ContainerRelationUnassignedVisibility extends ConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const ContainerRelationUnassignedVisibility({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isSiteAssigned = ref.watch(
|
||||
watchIsCurrentSiteAssignedToContainerProvider,
|
||||
);
|
||||
|
||||
return Visibility(
|
||||
visible: isSiteAssigned.hasValue && !isSiteAssigned.requireValue,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows [child] only once [watchIsCurrentSiteAssignedToContainerProvider]
|
||||
/// has resolved and the current tab's site is already assigned to a
|
||||
/// container. Shared between TabMenu and the browser menu bottom sheet.
|
||||
class ContainerRelationAssignedVisibility extends ConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const ContainerRelationAssignedVisibility({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isSiteAssigned = ref.watch(
|
||||
watchIsCurrentSiteAssignedToContainerProvider,
|
||||
);
|
||||
|
||||
return Visibility(
|
||||
visible: isSiteAssigned.hasValue && isSiteAssigned.requireValue,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows [child] only when [tabId] currently has a container assigned.
|
||||
/// Shared between TabMenu and the browser menu bottom sheet.
|
||||
class ContainerAssignedVisibility extends ConsumerWidget {
|
||||
final String tabId;
|
||||
final Widget child;
|
||||
|
||||
const ContainerAssignedVisibility({
|
||||
super.key,
|
||||
required this.tabId,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final containerId = ref.watch(
|
||||
watchContainerTabIdProvider(tabId).select((value) => value.value),
|
||||
);
|
||||
|
||||
return Visibility(visible: containerId != null, child: child);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user