refactor consumers into widgets
This commit is contained in:
+594
-316
@@ -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 {
|
class BrowserScreen extends HookConsumerWidget {
|
||||||
const BrowserScreen({super.key});
|
const BrowserScreen({super.key});
|
||||||
|
|
||||||
@@ -908,80 +1436,19 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
// Layer 0: Browser content
|
// Layer 0: Browser content
|
||||||
// Position changes instantly (no animation) to avoid jarring native view resize
|
// Position changes instantly (no animation) to avoid jarring native view resize
|
||||||
// The toolbar itself animates, providing visual continuity
|
// The toolbar itself animates, providing visual continuity
|
||||||
Consumer(
|
_BrowserContentPositioned(
|
||||||
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,
|
overlayController: overlayController,
|
||||||
tabInFullScreen: tabInFullScreen,
|
pointerMoveEventsController: pointerMoveEventsController,
|
||||||
pointerMoveEventSink: autoHideTabBar
|
selectedTabId: selectedTabId,
|
||||||
? pointerMoveEventsController.sink
|
|
||||||
: null,
|
|
||||||
sheetDisplayed: sheetDisplayed,
|
sheetDisplayed: sheetDisplayed,
|
||||||
hasTopBarOffset: topOffset > 0,
|
tabInFullScreen: tabInFullScreen,
|
||||||
hasLeftBarOffset: leftOffset > 0,
|
tabBarPosition: tabBarPosition,
|
||||||
hasRightBarOffset: rightOffset > 0,
|
autoHideTabBar: autoHideTabBar,
|
||||||
applyBottomSafeArea: applyBottomSafeArea,
|
isRail: isRail,
|
||||||
),
|
isSmallWebActive: isSmallWebActive,
|
||||||
);
|
sideRailTotalWidth: sideRailTotalWidth,
|
||||||
},
|
topAppBarTotalHeight: topAppBarTotalHeight,
|
||||||
|
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Layer 0.5: System bar tint — fills the status-bar/nav-bar
|
// Layer 0.5: System bar tint — fills the status-bar/nav-bar
|
||||||
@@ -1021,51 +1488,15 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: isSmallWebActive
|
child: _BottomToolbarLayer(
|
||||||
? _AnimatedToolbar(
|
sheetDisplayed: sheetDisplayed,
|
||||||
position: TabBarPosition.bottom,
|
tabInFullScreen: tabInFullScreen,
|
||||||
visible: !tabInFullScreen,
|
isSmallWebActive: isSmallWebActive,
|
||||||
child: Material(
|
tabBarPosition: tabBarPosition,
|
||||||
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,
|
showContextualToolbar: showContextualToolbar,
|
||||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||||
isSmallWebMode: false,
|
selectedTabId: selectedTabId,
|
||||||
pointerMoveEvents:
|
pointerMoveEventsController: pointerMoveEventsController,
|
||||||
tabBarPosition == TabBarPosition.bottom
|
|
||||||
? pointerMoveEventsController.stream
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -1075,36 +1506,14 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
child: Consumer(
|
child: _TopToolbarLayer(
|
||||||
// _TabBar is passed via `child` so it is built once and
|
sheetDisplayed: sheetDisplayed,
|
||||||
// reused across toolbar show/hide toggles; only the
|
tabInFullScreen: tabInFullScreen,
|
||||||
// `visible` flag fed to _AnimatedToolbar depends on the
|
isSmallWebActive: isSmallWebActive,
|
||||||
// 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,
|
showContextualToolbar: showContextualToolbar,
|
||||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||||
isSmallWebMode: isSmallWebActive,
|
selectedTabId: selectedTabId,
|
||||||
enableGestures: !isSmallWebActive,
|
pointerMoveEventsController: pointerMoveEventsController,
|
||||||
pointerMoveEvents: isSmallWebActive
|
|
||||||
? null
|
|
||||||
: pointerMoveEventsController.stream,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -1116,199 +1525,68 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: tabBarPosition == TabBarPosition.left ? 0 : null,
|
left: tabBarPosition == TabBarPosition.left ? 0 : null,
|
||||||
right: tabBarPosition == TabBarPosition.right ? 0 : null,
|
right: tabBarPosition == TabBarPosition.right ? 0 : null,
|
||||||
child: Consumer(
|
child: _SideRailToolbarLayer(
|
||||||
builder: (context, ref, child) {
|
sheetDisplayed: sheetDisplayed,
|
||||||
final toolbarState = ref.watch(
|
tabInFullScreen: tabInFullScreen,
|
||||||
toolbarVisibilityControllerProvider(selectedTabId),
|
tabBarPosition: tabBarPosition,
|
||||||
);
|
|
||||||
final visible =
|
|
||||||
sheetDisplayed ||
|
|
||||||
(!tabInFullScreen &&
|
|
||||||
toolbarState == ToolbarVisibility.visible);
|
|
||||||
return _AnimatedToolbar(
|
|
||||||
position: tabBarPosition,
|
|
||||||
visible: visible,
|
|
||||||
child: child!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: BrowserSideRail(
|
|
||||||
position: tabBarPosition,
|
|
||||||
showContextualToolbar: showContextualToolbar,
|
showContextualToolbar: showContextualToolbar,
|
||||||
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
quickTabSwitcherRowCount: quickTabSwitcherRowCount,
|
||||||
isSmallWebMode: false,
|
selectedTabId: selectedTabId,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Layer 4: FAB (draggable via long press)
|
// Layer 4: FAB (draggable via long press)
|
||||||
Consumer(
|
_FabPositioner(
|
||||||
builder: (context, ref, child) {
|
selectedTabId: selectedTabId,
|
||||||
final toolbarState = ref.watch(
|
sheetDisplayed: sheetDisplayed,
|
||||||
toolbarVisibilityControllerProvider(selectedTabId),
|
tabInFullScreen: tabInFullScreen,
|
||||||
);
|
tabBarPosition: tabBarPosition,
|
||||||
final visible =
|
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||||
sheetDisplayed ||
|
|
||||||
(!tabInFullScreen &&
|
|
||||||
toolbarState == ToolbarVisibility.visible);
|
|
||||||
return DraggableFab(
|
|
||||||
bottomToolbarVisible: visible,
|
|
||||||
bottomAppBarHeight: bottomAppBarTotalHeight,
|
|
||||||
bottomSafeArea: bottomSafeArea,
|
bottomSafeArea: bottomSafeArea,
|
||||||
leftReservedWidth:
|
sideRailTotalWidth: sideRailTotalWidth,
|
||||||
(tabBarPosition == TabBarPosition.left && visible)
|
|
||||||
? sideRailTotalWidth
|
|
||||||
: 0.0,
|
|
||||||
rightReservedWidth:
|
|
||||||
(tabBarPosition == TabBarPosition.right && visible)
|
|
||||||
? sideRailTotalWidth
|
|
||||||
: 0.0,
|
|
||||||
child: child!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const BrowserFab(),
|
child: const BrowserFab(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Layer 5: Page load progress indicator (animates with toolbar visibility)
|
// Layer 5: Page load progress indicator (animates with toolbar visibility)
|
||||||
Consumer(
|
_ProgressIndicatorPositioner(
|
||||||
builder: (context, ref, child) {
|
selectedTabId: selectedTabId,
|
||||||
final disableAnimations = MediaQuery.disableAnimationsOf(
|
sheetDisplayed: sheetDisplayed,
|
||||||
context,
|
tabInFullScreen: tabInFullScreen,
|
||||||
);
|
tabBarPosition: tabBarPosition,
|
||||||
final toolbarState = ref.watch(
|
isRail: isRail,
|
||||||
toolbarVisibilityControllerProvider(selectedTabId),
|
sideRailTotalWidth: sideRailTotalWidth,
|
||||||
);
|
topSafeArea: topSafeArea,
|
||||||
final visible =
|
topAppBarTotalHeight: topAppBarTotalHeight,
|
||||||
sheetDisplayed ||
|
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||||
(!tabInFullScreen &&
|
child: const _ProgressIndicatorBar(),
|
||||||
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),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Layer 6: Find in Page widget (above toolbar or keyboard, whichever is higher)
|
// Layer 6: Find in Page widget (above toolbar or keyboard, whichever is higher)
|
||||||
Consumer(
|
_FindInPagePositioner(
|
||||||
builder: (context, ref, child) {
|
selectedTabId: selectedTabId,
|
||||||
final disableAnimations = MediaQuery.disableAnimationsOf(
|
sheetDisplayed: sheetDisplayed,
|
||||||
context,
|
tabInFullScreen: tabInFullScreen,
|
||||||
);
|
tabBarPosition: tabBarPosition,
|
||||||
final toolbarState = ref.watch(
|
isRail: isRail,
|
||||||
toolbarVisibilityControllerProvider(selectedTabId),
|
sideRailTotalWidth: sideRailTotalWidth,
|
||||||
);
|
bottomSafeArea: bottomSafeArea,
|
||||||
final visible =
|
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||||
sheetDisplayed ||
|
child: const _FindInPageContent(),
|
||||||
(!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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Layer 7: App-link prompt banner (§2.6). Anchored above the bottom app
|
// 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
|
// 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 toolbar. Custom Tab sessions are prompted natively instead; this is
|
||||||
// the browser-tab surface only.
|
// the browser-tab surface only.
|
||||||
Consumer(
|
_AppLinkPromptLayer(
|
||||||
builder: (context, ref, child) {
|
selectedTabId: selectedTabId,
|
||||||
final toolbarState = ref.watch(
|
sheetDisplayed: sheetDisplayed,
|
||||||
toolbarVisibilityControllerProvider(selectedTabId),
|
tabInFullScreen: tabInFullScreen,
|
||||||
);
|
tabBarPosition: tabBarPosition,
|
||||||
final visible =
|
isRail: isRail,
|
||||||
sheetDisplayed ||
|
sideRailTotalWidth: sideRailTotalWidth,
|
||||||
(!tabInFullScreen &&
|
bottomSafeArea: bottomSafeArea,
|
||||||
toolbarState == ToolbarVisibility.visible);
|
bottomAppBarTotalHeight: bottomAppBarTotalHeight,
|
||||||
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(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
+13
-46
@@ -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/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.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/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/container.dart';
|
||||||
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/presentation/widgets/container_relation_visibility.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/top_sites/domain/repositories/top_site_repository.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/data/models/gesture_settings.dart';
|
||||||
import 'package:weblibre/features/gestures/domain/repositories/gesture_settings.dart';
|
import 'package:weblibre/features/gestures/domain/repositories/gesture_settings.dart';
|
||||||
@@ -1271,17 +1271,8 @@ class _ContainerExpansion extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// URL relation (conditional)
|
// URL relation (conditional)
|
||||||
Consumer(
|
ContainerRelationUnassignedVisibility(
|
||||||
builder: (context, ref, child) {
|
child: _buildSubTile(
|
||||||
final isSiteAssigned = ref.watch(
|
|
||||||
watchIsCurrentSiteAssignedToContainerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isSiteAssigned.hasValue || isSiteAssigned.requireValue) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _buildSubTile(
|
|
||||||
'Assign URL to Container',
|
'Assign URL to Container',
|
||||||
icon: MdiIcons.webPlus,
|
icon: MdiIcons.webPlus,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -1296,9 +1287,7 @@ class _ContainerExpansion extends ConsumerWidget {
|
|||||||
.getContainerData(containerId);
|
.getContainerData(containerId);
|
||||||
|
|
||||||
if (containerData != null) {
|
if (containerData != null) {
|
||||||
final tabState = ref.read(
|
final tabState = ref.read(tabStateProvider(selectedTabId));
|
||||||
tabStateProvider(selectedTabId),
|
|
||||||
);
|
|
||||||
final origin = tabState?.url.origin.mapNotNull(Uri.parse);
|
final origin = tabState?.url.origin.mapNotNull(Uri.parse);
|
||||||
|
|
||||||
if (origin != null) {
|
if (origin != null) {
|
||||||
@@ -1318,22 +1307,12 @@ class _ContainerExpansion extends ConsumerWidget {
|
|||||||
|
|
||||||
if (context.mounted) Navigator.pop(context);
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Unassign URL relation (conditional)
|
// Unassign URL relation (conditional)
|
||||||
Consumer(
|
ContainerRelationAssignedVisibility(
|
||||||
builder: (context, ref, child) {
|
child: _buildSubTile(
|
||||||
final isSiteAssigned = ref.watch(
|
|
||||||
watchIsCurrentSiteAssignedToContainerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isSiteAssigned.hasValue || !isSiteAssigned.requireValue) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _buildSubTile(
|
|
||||||
'Unassign URL from Container',
|
'Unassign URL from Container',
|
||||||
icon: MdiIcons.webMinus,
|
icon: MdiIcons.webMinus,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -1351,9 +1330,7 @@ class _ContainerExpansion extends ConsumerWidget {
|
|||||||
.getContainerData(containerId);
|
.getContainerData(containerId);
|
||||||
|
|
||||||
if (containerData != null) {
|
if (containerData != null) {
|
||||||
final updatedSites = containerData
|
final updatedSites = containerData.metadata.assignedSites
|
||||||
.metadata
|
|
||||||
.assignedSites
|
|
||||||
?.where((site) => site != origin)
|
?.where((site) => site != origin)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -1372,22 +1349,13 @@ class _ContainerExpansion extends ConsumerWidget {
|
|||||||
|
|
||||||
if (context.mounted) Navigator.pop(context);
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Unassign Container (conditional)
|
// Unassign Container (conditional)
|
||||||
Consumer(
|
ContainerAssignedVisibility(
|
||||||
builder: (context, ref, child) {
|
tabId: selectedTabId,
|
||||||
final containerId = ref.watch(
|
child: _buildSubTile(
|
||||||
watchContainerTabIdProvider(
|
|
||||||
selectedTabId,
|
|
||||||
).select((value) => value.value),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (containerId == null) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
return _buildSubTile(
|
|
||||||
'Unassign Container',
|
'Unassign Container',
|
||||||
icon: MdiIcons.folderCancelOutline,
|
icon: MdiIcons.folderCancelOutline,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -1397,8 +1365,7 @@ class _ContainerExpansion extends ConsumerWidget {
|
|||||||
.unassignContainer(tabState.id);
|
.unassignContainer(tabState.id);
|
||||||
if (context.mounted) Navigator.pop(context);
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
+234
-208
@@ -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/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.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/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/data/models/general_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/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';
|
import 'package:weblibre/features/web_search/domain/controllers/sandbox_capture_controller.dart';
|
||||||
@@ -155,32 +156,16 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (enableDesktopMode)
|
if (enableDesktopMode)
|
||||||
Consumer(
|
_DesktopModeMenuItem(
|
||||||
builder: (context, childRef, child) {
|
selectedTabId: selectedTabId,
|
||||||
final enabled = childRef.watch(
|
controller: controller,
|
||||||
desktopModeProvider(selectedTabId),
|
onToggle: () {
|
||||||
);
|
ref.read(desktopModeProvider(selectedTabId).notifier).toggle();
|
||||||
|
|
||||||
return MenuItemButton(
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.read(desktopModeProvider(selectedTabId).notifier)
|
|
||||||
.toggle();
|
|
||||||
},
|
},
|
||||||
leadingIcon: const Icon(MdiIcons.monitor),
|
onEnabledChanged: (value) {
|
||||||
trailingIcon: Checkbox(
|
|
||||||
value: enabled,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value != null) {
|
|
||||||
ref
|
ref
|
||||||
.read(desktopModeProvider(selectedTabId).notifier)
|
.read(desktopModeProvider(selectedTabId).notifier)
|
||||||
.enabled(value);
|
.enabled(value);
|
||||||
controller.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
child: const Text('Desktop Mode'),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (enableFindInPage || enableReaderMode || enableDesktopMode)
|
if (enableFindInPage || enableReaderMode || enableDesktopMode)
|
||||||
@@ -220,35 +205,7 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
).push(context);
|
).push(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (enableAddToHomeScreen)
|
if (enableAddToHomeScreen) const _AddToHomeScreenMenuItem(),
|
||||||
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 (enableCloneTab)
|
if (enableCloneTab)
|
||||||
SubmenuButton(
|
SubmenuButton(
|
||||||
menuChildren: [
|
menuChildren: [
|
||||||
@@ -434,7 +391,7 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Consumer(
|
ContainerRelationUnassignedVisibility(
|
||||||
child: MenuItemButton(
|
child: MenuItemButton(
|
||||||
leadingIcon: const Icon(MdiIcons.webPlus),
|
leadingIcon: const Icon(MdiIcons.webPlus),
|
||||||
child: const Text('URL relation'),
|
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(
|
child: MenuItemButton(
|
||||||
leadingIcon: const Icon(MdiIcons.webMinus),
|
leadingIcon: const Icon(MdiIcons.webMinus),
|
||||||
child: const Text('Unassign URL relation'),
|
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(
|
child: MenuItemButton(
|
||||||
leadingIcon: const Icon(MdiIcons.folderCancelOutline),
|
leadingIcon: const Icon(MdiIcons.folderCancelOutline),
|
||||||
child: const Text('Unassign Container'),
|
child: const Text('Unassign Container'),
|
||||||
@@ -550,74 +486,26 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
.unassignContainer(tabState.id);
|
.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),
|
leadingIcon: const Icon(MdiIcons.folder),
|
||||||
child: const Text('Container'),
|
child: const Text('Container'),
|
||||||
),
|
),
|
||||||
if (enableHierarchy)
|
if (enableHierarchy)
|
||||||
Consumer(
|
_TabHierarchySubmenu(
|
||||||
builder: (childContext, childRef, child) {
|
selectedTabId: selectedTabId,
|
||||||
// `MenuItemButton.onPressed` is dispatched as a post-frame
|
controller: controller,
|
||||||
// callback by Flutter's menu_anchor — by the time it fires,
|
onChangeParent: () async {
|
||||||
// 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(
|
await showTabParentPicker(
|
||||||
context: context,
|
context: context,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
tabId: selectedTabId,
|
tabId: selectedTabId,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Change parent…'),
|
onDetachFromParent: () async {
|
||||||
),
|
await ref
|
||||||
MenuItemButton(
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
leadingIcon: const Icon(MdiIcons.fileTreeOutline),
|
.setTabParent(tabId: selectedTabId, newParentId: null);
|
||||||
onPressed: hasParent
|
|
||||||
? () async {
|
|
||||||
await repo.setTabParent(
|
|
||||||
tabId: selectedTabId,
|
|
||||||
newParentId: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: const Text('Detach from parent'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: const Text('Hierarchy'),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (enableReorder)
|
if (enableReorder)
|
||||||
@@ -708,73 +596,11 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
leadingIcon: const Icon(MdiIcons.fileExport),
|
leadingIcon: const Icon(MdiIcons.fileExport),
|
||||||
child: const Text('Export'),
|
child: const Text('Export'),
|
||||||
),
|
),
|
||||||
Consumer(
|
_TranslatePageMenuItem(
|
||||||
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,
|
selectedTabId: selectedTabId,
|
||||||
);
|
controller: controller,
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(isTranslated ? 'Translated' : 'Translate Page'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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)
|
if (enableCloseTab)
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
@@ -797,18 +623,222 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
child: const Text('Reload'),
|
child: const Text('Reload'),
|
||||||
),
|
),
|
||||||
if (enableNavigationButtons)
|
if (enableNavigationButtons)
|
||||||
Consumer(
|
_NavigationButtonsRow(
|
||||||
builder: (context, ref, child) {
|
selectedTabId: selectedTabId,
|
||||||
final history = ref.watch(
|
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(
|
tabStateProvider(
|
||||||
selectedTabId,
|
selectedTabId,
|
||||||
).select((value) => value?.historyState),
|
).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(
|
final isLoading = ref.watch(
|
||||||
selectedTabStateProvider.select(
|
selectedTabStateProvider.select((state) => state?.isLoading ?? false),
|
||||||
(state) => state?.isLoading ?? false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
@@ -831,9 +861,5 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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