refactor tab bar behaviour; respect scroll preferences of websites;

This commit is contained in:
Fabian Freund
2026-02-19 06:20:08 +01:00
parent 4d7c185460
commit 3025d6e42d
21 changed files with 602 additions and 195 deletions
@@ -64,6 +64,7 @@ class TabState extends WebPageInfo {
final bool isPrivate;
final bool isFullScreen;
final bool isLoading;
final bool showToolbarAsExpanded;
bool get isFinishedLoading => !isLoading && progress == 100;
@@ -84,6 +85,7 @@ class TabState extends WebPageInfo {
required this.isPrivate,
required this.isFullScreen,
required this.isLoading,
required this.showToolbarAsExpanded,
required this.securityInfoState,
required this.historyState,
required this.readerableState,
@@ -102,6 +104,7 @@ class TabState extends WebPageInfo {
isPrivate: false,
isFullScreen: false,
isLoading: false,
showToolbarAsExpanded: false,
securityInfoState: SecurityState.$default(),
historyState: HistoryState.$default(),
readerableState: ReaderableState.$default(),
@@ -120,6 +123,7 @@ class TabState extends WebPageInfo {
isPrivate,
isFullScreen,
isLoading,
showToolbarAsExpanded,
securityInfoState,
historyState,
readerableState,
@@ -27,6 +27,8 @@ abstract class _$TabStateCWProxy {
TabState isLoading(bool isLoading);
TabState showToolbarAsExpanded(bool showToolbarAsExpanded);
TabState securityInfoState(SecurityState securityInfoState);
TabState historyState(HistoryState historyState);
@@ -53,6 +55,7 @@ abstract class _$TabStateCWProxy {
bool isPrivate,
bool isFullScreen,
bool isLoading,
bool showToolbarAsExpanded,
SecurityState securityInfoState,
HistoryState historyState,
ReaderableState readerableState,
@@ -97,6 +100,10 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
@override
TabState isLoading(bool isLoading) => call(isLoading: isLoading);
@override
TabState showToolbarAsExpanded(bool showToolbarAsExpanded) =>
call(showToolbarAsExpanded: showToolbarAsExpanded);
@override
TabState securityInfoState(SecurityState securityInfoState) =>
call(securityInfoState: securityInfoState);
@@ -132,6 +139,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
Object? isPrivate = const $CopyWithPlaceholder(),
Object? isFullScreen = const $CopyWithPlaceholder(),
Object? isLoading = const $CopyWithPlaceholder(),
Object? showToolbarAsExpanded = const $CopyWithPlaceholder(),
Object? securityInfoState = const $CopyWithPlaceholder(),
Object? historyState = const $CopyWithPlaceholder(),
Object? readerableState = const $CopyWithPlaceholder(),
@@ -180,6 +188,12 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
? _value.isLoading
// ignore: cast_nullable_to_non_nullable
: isLoading as bool,
showToolbarAsExpanded:
showToolbarAsExpanded == const $CopyWithPlaceholder() ||
showToolbarAsExpanded == null
? _value.showToolbarAsExpanded
// ignore: cast_nullable_to_non_nullable
: showToolbarAsExpanded as bool,
securityInfoState:
securityInfoState == const $CopyWithPlaceholder() ||
securityInfoState == null
@@ -87,6 +87,7 @@ class TabStates extends _$TabStates {
isPrivate: contentState.isPrivate,
isFullScreen: contentState.isFullScreen,
isLoading: contentState.isLoading,
showToolbarAsExpanded: contentState.showToolbarAsExpanded,
);
_updateState({...state}..[contentState.id] = newState);
@@ -41,7 +41,7 @@ final class TabStatesProvider
}
}
String _$tabStatesHash() => r'985d2eb87c59c3a113b8bbcf9d96267c42a2f543';
String _$tabStatesHash() => r'975de1242f42e2017754dc560125788b83ad8b72';
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
Map<String, TabState> build();
@@ -1,40 +0,0 @@
/*
* 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:riverpod_annotation/riverpod_annotation.dart';
part 'tab_bar_dismissable.g.dart';
@Riverpod(keepAlive: true)
class TabBarDismissableController extends _$TabBarDismissableController {
void show() {
state = false;
}
void dismiss() {
if (!state) {
state = true;
}
}
@override
bool build() {
return false;
}
}
@@ -1,64 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab_bar_dismissable.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(TabBarDismissableController)
final tabBarDismissableControllerProvider =
TabBarDismissableControllerProvider._();
final class TabBarDismissableControllerProvider
extends $NotifierProvider<TabBarDismissableController, bool> {
TabBarDismissableControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabBarDismissableControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabBarDismissableControllerHash();
@$internal
@override
TabBarDismissableController create() => TabBarDismissableController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$tabBarDismissableControllerHash() =>
r'e0c2f700ccebc35bc8029370cdebd63e98bf95e1';
abstract class _$TabBarDismissableController extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,108 @@
/*
* 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:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
part 'toolbar_visibility.g.dart';
enum ToolbarVisibility { visible, hidden, dismissed }
@Riverpod(keepAlive: true)
class ToolbarVisibilityController extends _$ToolbarVisibilityController {
@override
ToolbarVisibility build(String? tabId) {
// Show toolbar when loading starts
ref.listen(tabStatesProvider.select((tabs) => tabs[tabId]?.isLoading), (
previous,
next,
) {
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) {
return;
}
if (next == true) {
show();
}
});
// Show toolbar on navigation (history state change)
ref.listen(tabStatesProvider.select((tabs) => tabs[tabId]?.historyState), (
previous,
next,
) {
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) {
return;
}
if (next != null && previous != null && previous != next) {
show();
}
});
// Force-show when GeckoView requests toolbar expansion
// (e.g. touch on form input)
ref.listen<bool>(
tabStatesProvider.select(
(tabs) => tabs[tabId]?.showToolbarAsExpanded ?? false,
),
(previous, next) {
if (next && previous != next) {
forceShow();
}
},
);
return ToolbarVisibility.visible;
}
/// Hide toolbar via scroll. All guards checked internally.
void requestHide() {
final settings = ref.read(generalSettingsWithDefaultsProvider);
if (!settings.autoHideTabBar) return;
final isLoading = ref.read(tabStatesProvider)[tabId]?.isLoading ?? false;
if (isLoading) return;
final viewportService = ref.read(viewportServiceProvider);
if (!viewportService.isBrowserHandlingScrollEnabled) return;
if (state == ToolbarVisibility.dismissed) return;
state = ToolbarVisibility.hidden;
}
/// Show toolbar (scroll-up, tab change, loading start, etc).
/// Won't show if manually dismissed — use forceShow() for that.
void show() {
if (state == ToolbarVisibility.dismissed) return;
state = ToolbarVisibility.visible;
}
/// Force-show unconditionally + un-dismiss.
void forceShow() {
state = ToolbarVisibility.visible;
}
/// Dismiss toolbar (user swipe). Only affects this tab.
void dismiss() {
state = ToolbarVisibility.dismissed;
}
}
@@ -0,0 +1,111 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'toolbar_visibility.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ToolbarVisibilityController)
final toolbarVisibilityControllerProvider =
ToolbarVisibilityControllerFamily._();
final class ToolbarVisibilityControllerProvider
extends $NotifierProvider<ToolbarVisibilityController, ToolbarVisibility> {
ToolbarVisibilityControllerProvider._({
required ToolbarVisibilityControllerFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'toolbarVisibilityControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$toolbarVisibilityControllerHash();
@override
String toString() {
return r'toolbarVisibilityControllerProvider'
''
'($argument)';
}
@$internal
@override
ToolbarVisibilityController create() => ToolbarVisibilityController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ToolbarVisibility value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ToolbarVisibility>(value),
);
}
@override
bool operator ==(Object other) {
return other is ToolbarVisibilityControllerProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$toolbarVisibilityControllerHash() =>
r'242449893afd6780d977544659e83d4641c8a0ad';
final class ToolbarVisibilityControllerFamily extends $Family
with
$ClassFamilyOverride<
ToolbarVisibilityController,
ToolbarVisibility,
ToolbarVisibility,
ToolbarVisibility,
String?
> {
ToolbarVisibilityControllerFamily._()
: super(
retry: null,
name: r'toolbarVisibilityControllerProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: false,
);
ToolbarVisibilityControllerProvider call(String? tabId) =>
ToolbarVisibilityControllerProvider._(argument: tabId, from: this);
@override
String toString() => r'toolbarVisibilityControllerProvider';
}
abstract class _$ToolbarVisibilityController
extends $Notifier<ToolbarVisibility> {
late final _$args = ref.$arg as String?;
String? get tabId => _$args;
ToolbarVisibility build(String? tabId);
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<ToolbarVisibility, ToolbarVisibility>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ToolbarVisibility, ToolbarVisibility>,
ToolbarVisibility,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@@ -39,7 +39,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
@@ -136,7 +136,6 @@ class _TabBar extends HookConsumerWidget {
final bool showMainToolbar;
final bool showContextualToolbar;
final bool showQuickTabSwitcherBar;
final ValueNotifier<bool> displayAppBar;
final Stream<Offset>? pointerMoveEvents;
final TabBarPosition tabBarPosition;
@@ -144,7 +143,6 @@ class _TabBar extends HookConsumerWidget {
required this.showMainToolbar,
required this.showContextualToolbar,
required this.showQuickTabSwitcherBar,
required this.displayAppBar,
required this.tabBarPosition,
required this.pointerMoveEvents,
});
@@ -154,30 +152,34 @@ class _TabBar extends HookConsumerWidget {
final tabId = ref.watch(selectedTabProvider);
final displayedSheet = ref.watch(bottomSheetControllerProvider);
final autoHideTabBar = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.autoHideTabBar,
),
);
// Auto-hide scroll detection hooks (run unconditionally per hook rules)
final diffAcc = useRef(0.0);
void resetHiddenState() {
if (!ref.read(tabBarDismissableControllerProvider)) {
displayAppBar.value = true;
// Reset scroll accumulator on tab switch
useEffect(() {
diffAcc.value = 0.0;
return null;
}, [tabId]);
// Reset scroll accumulator when controller shows toolbar
// (covers: loading start, navigation, showToolbarAsExpanded, forceShow)
ref.listen(toolbarVisibilityControllerProvider(tabId), (previous, next) {
if (next == ToolbarVisibility.visible && previous != next) {
diffAcc.value = 0.0;
}
});
void resetHiddenState() {
ref
.read(
toolbarVisibilityControllerProvider(
ref.read(selectedTabProvider),
).notifier,
)
.show();
diffAcc.value = 0.0;
}
useEffect(() {
if (!autoHideTabBar) return null;
WidgetsBinding.instance.addPostFrameCallback((_) {
resetHiddenState();
});
return null;
}, [tabId, autoHideTabBar]);
useOnAppLifecycleStateChange((previous, current) {
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
if (current == AppLifecycleState.resumed) {
@@ -185,38 +187,13 @@ class _TabBar extends HookConsumerWidget {
}
});
ref.listen(tabStateProvider(tabId).select((value) => value?.isLoading), (
previous,
next,
) {
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
if (next == true) {
resetHiddenState();
}
});
ref.listen(tabStateProvider(tabId).select((value) => value?.historyState), (
previous,
next,
) {
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
if (next != null && previous != null) {
if (previous != next) {
resetHiddenState();
}
}
});
useOnStreamChange(
pointerMoveEvents,
onData: (event) {
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) {
return;
}
// Don't apply auto-hide when toolbar is manually dismissed
if (ref.read(tabBarDismissableControllerProvider)) {
return;
}
final autoHide = ref
.read(generalSettingsWithDefaultsProvider)
.autoHideTabBar;
if (!autoHide) return;
final diff = event.dy;
if (diff < 0) {
@@ -225,7 +202,13 @@ class _TabBar extends HookConsumerWidget {
}
diffAcc.value += diff;
if (diffAcc.value.abs() > kToolbarHeight * 1.5) {
displayAppBar.value = false;
ref
.read(
toolbarVisibilityControllerProvider(
ref.read(selectedTabProvider),
).notifier,
)
.requestHide();
}
} else if (diff > 0) {
if (diffAcc.value < 0) {
@@ -267,6 +250,9 @@ class BrowserScreen extends HookConsumerWidget {
final tabInFullScreen = ref.watch(
selectedTabStateProvider.select((value) => value?.isFullScreen ?? false),
);
final tabIsLoading = ref.watch(
selectedTabStateProvider.select((value) => value?.isLoading ?? false),
);
final overlayController = useOverlayPortalController();
@@ -300,12 +286,6 @@ class BrowserScreen extends HookConsumerWidget {
),
);
final displayAppBar = useValueNotifier(true);
ref.listen(tabBarDismissableControllerProvider, (previous, next) {
displayAppBar.value = !next;
});
ref.listen(overlayControllerProvider, (previous, next) {
if (next != null) {
overlayController.show();
@@ -330,14 +310,19 @@ class BrowserScreen extends HookConsumerWidget {
final sheetDisplayed = displayedSheet != null;
// Compute visibility states for toolbars
final appBarVisible = useValueListenable(displayAppBar);
final selectedTabId = ref.watch(selectedTabProvider);
final toolbarState = ref.watch(
toolbarVisibilityControllerProvider(selectedTabId),
);
// Toolbar is visible when: sheet is shown OR (not fullscreen AND app bar visible)
// appBarVisible reflects both scroll-based auto-hide and swipe dismiss state
// Toolbar is visible when: sheet is shown OR (not fullscreen AND controller says visible)
// The controller handles loading-start show internally via ref.listen on isLoading
final effectiveAppBarVisible =
toolbarState == ToolbarVisibility.visible;
final topToolbarVisible =
sheetDisplayed || (!tabInFullScreen && appBarVisible);
sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible);
final bottomToolbarVisible =
sheetDisplayed || (!tabInFullScreen && appBarVisible);
sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible);
// Calculate relative safe area for sheet max size
final relativeSafeArea = MediaQuery.of(context).relativeSafeArea();
@@ -382,7 +367,6 @@ class BrowserScreen extends HookConsumerWidget {
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
// Watch find-in-page visibility for the selected tab
final selectedTabId = ref.watch(selectedTabProvider);
final findInPageVisible =
selectedTabId != null &&
ref.watch(
@@ -446,7 +430,8 @@ class BrowserScreen extends HookConsumerWidget {
// Keep clipping state in sync with keyboard and auto-hide states.
// While keyboard is visible, do not apply toolbar clipping.
useEffect(() {
final targetClippingPx = autoHideTabBar && !keyboardVisible
final targetClippingPx =
autoHideTabBar && !keyboardVisible && !tabIsLoading
? desiredToolbarClippingPx.value
: 0;
@@ -456,7 +441,7 @@ class BrowserScreen extends HookConsumerWidget {
}
return null;
}, [autoHideTabBar, keyboardVisible]);
}, [autoHideTabBar, keyboardVisible, tabIsLoading]);
final animationProgressCallback = useCallback((
double progress,
@@ -467,13 +452,15 @@ class BrowserScreen extends HookConsumerWidget {
final clippingPx = -((1.0 - progress) * heightPx * pixelRatio).round();
desiredToolbarClippingPx.value = clippingPx;
final targetClippingPx = keyboardVisible ? 0 : clippingPx;
final targetClippingPx = (keyboardVisible || tabIsLoading)
? 0
: clippingPx;
if (targetClippingPx != lastClippingPx.value) {
lastClippingPx.value = targetClippingPx;
unawaited(viewportService.setVerticalClipping(targetClippingPx));
}
}, [keyboardVisible, pixelRatio]);
}, [keyboardVisible, pixelRatio, tabIsLoading]);
// Theme with dynamic snackbar margin to position above bottom toolbar
final themeData = Theme.of(context).copyWith(
@@ -522,7 +509,6 @@ class BrowserScreen extends HookConsumerWidget {
bottom: browserBottomOffset,
child: _Browser(
overlayController: overlayController,
displayAppBar: displayAppBar,
tabInFullScreen: tabInFullScreen,
pointerMoveEventSink: pointerMoveEventsController.sink,
sheetDisplayed: sheetDisplayed,
@@ -558,7 +544,6 @@ class BrowserScreen extends HookConsumerWidget {
: null,
child: _TabBar(
tabBarPosition: TabBarPosition.bottom,
displayAppBar: displayAppBar,
showMainToolbar: tabBarPosition == TabBarPosition.bottom,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
@@ -583,7 +568,6 @@ class BrowserScreen extends HookConsumerWidget {
child: _TabBar(
tabBarPosition: TabBarPosition.top,
showMainToolbar: true,
displayAppBar: displayAppBar,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
pointerMoveEvents: pointerMoveEventsController.stream,
@@ -729,7 +713,6 @@ class _Browser extends HookConsumerWidget {
Duration get _backButtonPressTimeout => const Duration(seconds: 2);
final OverlayPortalController overlayController;
final ValueNotifier<bool> displayAppBar;
final StreamSink<Offset> pointerMoveEventSink;
final bool tabInFullScreen;
final bool sheetDisplayed;
@@ -737,7 +720,6 @@ class _Browser extends HookConsumerWidget {
const _Browser({
required this.overlayController,
required this.displayAppBar,
required this.tabInFullScreen,
required this.pointerMoveEventSink,
required this.sheetDisplayed,
@@ -822,9 +804,10 @@ class _Browser extends HookConsumerWidget {
}
//Make sure app bar is visible
if (!ref.read(tabBarDismissableControllerProvider)) {
displayAppBar.value = true;
}
final tabId = ref.read(selectedTabProvider);
ref
.read(toolbarVisibilityControllerProvider(tabId).notifier)
.show();
if (tabState?.isLoading == true) {
lastBackButtonPress.value = null;
@@ -32,12 +32,13 @@ import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart';
@@ -59,6 +60,7 @@ import 'package:weblibre/presentation/icons/weblibre_icons.dart';
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
import 'package:weblibre/utils/ui_helper.dart';
class BrowserTopAppBar extends HookConsumerWidget {
final bool showMainToolbar;
@@ -68,7 +70,8 @@ class BrowserTopAppBar extends HookConsumerWidget {
late final BrowserTabBar _tabBar;
late final _size = Size.fromHeight(_tabBar.getToolbarHeight());
BrowserTopAppBar({super.key,
BrowserTopAppBar({
super.key,
required this.showMainToolbar,
required this.showContextualToolbar,
required this.showQuickTabSwitcherBar,
@@ -103,7 +106,8 @@ class BrowserBottomAppBar extends HookConsumerWidget {
late final BrowserTabBar _tabBar;
late final _size = Size.fromHeight(_tabBar.getToolbarHeight());
BrowserBottomAppBar({super.key,
BrowserBottomAppBar({
super.key,
required this.showMainToolbar,
required this.displayedSheet,
required this.showContextualToolbar,
@@ -148,7 +152,8 @@ class BrowserTabBar extends HookConsumerWidget {
final bool showMainToolbarNavigationButton;
final bool showMainToolbarTabActionButton;
const BrowserTabBar({super.key,
const BrowserTabBar({
super.key,
required this.showMainToolbar,
required this.displayedSheet,
required this.showContextualToolbar,
@@ -270,8 +275,23 @@ class BrowserTabBar extends HookConsumerWidget {
!distance.dy.isNegative && distance.dy.abs() > dismissThreshold,
};
if (shouldDismiss && ref.read(bottomSheetControllerProvider) == null) {
unawaited(HapticFeedback.lightImpact());
ref.read(tabBarDismissableControllerProvider.notifier).dismiss();
final viewportService = ref.read(viewportServiceProvider);
if (viewportService.isBrowserHandlingScrollEnabled) {
unawaited(HapticFeedback.lightImpact());
ref
.read(
toolbarVisibilityControllerProvider(selectedTabId).notifier,
)
.dismiss();
} else if (context.mounted) {
showDismissOverrideMessage(context, () {
ref
.read(
toolbarVisibilityControllerProvider(selectedTabId).notifier,
)
.dismiss();
});
}
}
},
child: Column(
@@ -21,8 +21,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
import 'package:weblibre/features/geckoview/features/readerview/domain/providers/readerable.dart';
class BrowserFab extends HookConsumerWidget {
@@ -30,7 +31,10 @@ class BrowserFab extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarDismissed = ref.watch(tabBarDismissableControllerProvider);
final selectedTabId = ref.watch(selectedTabProvider);
final toolbarState = ref.watch(
toolbarVisibilityControllerProvider(selectedTabId),
);
final appearanceButtonVisible = ref.watch(
appearanceButtonVisibilityProvider.select(
@@ -54,12 +58,16 @@ class BrowserFab extends HookConsumerWidget {
},
child: const Icon(MdiIcons.formatFont),
);
} else if (tabBarDismissed) {
} else if (toolbarState == ToolbarVisibility.dismissed) {
child = FloatingActionButton(
key: const ValueKey('dock_fab'),
heroTag: 'dock_fab',
onPressed: () {
ref.read(tabBarDismissableControllerProvider.notifier).show();
ref
.read(
toolbarVisibilityControllerProvider(selectedTabId).notifier,
)
.forceShow();
},
child: const Icon(MdiIcons.dockBottom),
);
@@ -33,7 +33,7 @@ final class ProfileRepositoryProvider
ProfileRepository create() => ProfileRepository();
}
String _$profileRepositoryHash() => r'c17702af1e59727ab3fec26e9ca659048e92c8bb';
String _$profileRepositoryHash() => r'b770e7406e1602f808cc8076c1eda67b4fce6b2d';
abstract class _$ProfileRepository extends $AsyncNotifier<List<Profile>> {
FutureOr<List<Profile>> build();
+16
View File
@@ -192,3 +192,19 @@ void showTabUndoClose(
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
void showDismissOverrideMessage(
BuildContext context,
VoidCallback onDismiss, {
Duration duration = const Duration(seconds: 4),
}) {
ScaffoldMessenger.of(context).clearSnackBars();
final snackBar = _createFloatingSnackBar(
content: const Text('Hiding disabled by site'),
action: SnackBarAction(label: 'Dismiss', onPressed: onDismiss),
duration: duration,
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}