app bar positioning

This commit is contained in:
Fabian Freund
2025-12-22 21:00:02 +01:00
parent a90f18d9c8
commit 5a46751834
11 changed files with 540 additions and 290 deletions
@@ -300,13 +300,13 @@ AsyncValue<String?> selectedTabContainerId(Ref ref) {
return const AsyncData(null);
}
@Riverpod()
Stream<int> tabScrollY(Ref ref, String? tabId, Duration sampleTime) {
final eventService = ref.watch(eventServiceProvider);
// @Riverpod()
// Stream<int> tabScrollY(Ref ref, String? tabId, Duration sampleTime) {
// final eventService = ref.watch(eventServiceProvider);
return eventService.scrollEvent
.where((event) => event.tabId == tabId)
.sampleTime(sampleTime)
.map((event) => event.scrollY)
.asBroadcastStream();
}
// return eventService.scrollEvent
// .where((event) => event.tabId == tabId)
// .sampleTime(sampleTime)
// .map((event) => event.scrollY)
// .asBroadcastStream();
// }
@@ -337,72 +337,3 @@ final class SelectedTabContainerIdProvider
String _$selectedTabContainerIdHash() =>
r'07899d29f69654d3b314d0da945ad402b1003b41';
@ProviderFor(tabScrollY)
const tabScrollYProvider = TabScrollYFamily._();
final class TabScrollYProvider
extends $FunctionalProvider<AsyncValue<int>, int, Stream<int>>
with $FutureModifier<int>, $StreamProvider<int> {
const TabScrollYProvider._({
required TabScrollYFamily super.from,
required (String?, Duration) super.argument,
}) : super(
retry: null,
name: r'tabScrollYProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabScrollYHash();
@override
String toString() {
return r'tabScrollYProvider'
''
'$argument';
}
@$internal
@override
$StreamProviderElement<int> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<int> create(Ref ref) {
final argument = this.argument as (String?, Duration);
return tabScrollY(ref, argument.$1, argument.$2);
}
@override
bool operator ==(Object other) {
return other is TabScrollYProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$tabScrollYHash() => r'dcb1e82b42bab98c4fc2ee4c7a22f37a28b8ce9e';
final class TabScrollYFamily extends $Family
with $FunctionalFamilyOverride<Stream<int>, (String?, Duration)> {
const TabScrollYFamily._()
: super(
retry: null,
name: r'tabScrollYProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
TabScrollYProvider call(String? tabId, Duration sampleTime) =>
TabScrollYProvider._(argument: (tabId, sampleTime), from: this);
@override
String toString() => r'tabScrollYProvider';
}
@@ -50,20 +50,27 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
class _TabBar extends HookConsumerWidget {
final bool showMainToolbar;
final bool showContextualToolbar;
final bool showQuickTabSwitcherBar;
final ValueNotifier<bool> showAppBar;
final ValueNotifier<bool> displayAppBar;
final ValueNotifier<PersistentBottomSheetController?> sheetController;
final Stream<Offset>? pointerMoveEvents;
final TabBarPosition tabBarPosition;
const _TabBar({
required this.showMainToolbar,
required this.showContextualToolbar,
required this.showQuickTabSwitcherBar,
required this.showAppBar,
required this.displayAppBar,
required this.sheetController,
required this.tabBarPosition,
required this.pointerMoveEvents,
});
@override
@@ -75,29 +82,43 @@ class _TabBar extends HookConsumerWidget {
selectedTabStateProvider.select((value) => value?.isFullScreen ?? false),
);
final autoHideTabBar = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.autoHideTabBar,
final autoHideTabBar = switch (tabBarPosition) {
TabBarPosition.top => false,
TabBarPosition.bottom => ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.autoHideTabBar,
),
),
);
};
final appBarVisible = tabBarPosition == TabBarPosition.top
? !ref.watch(tabBarDismissableControllerProvider)
: useValueListenable(displayAppBar);
if (!autoHideTabBar) {
return Visibility(
visible: !tabInFullScreen,
child: BrowserBottomAppBar(
displayedSheet: displayedSheet,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
visible: !tabInFullScreen && appBarVisible,
child: switch (tabBarPosition) {
TabBarPosition.top => BrowserTopAppBar(
showMainToolbar: showMainToolbar,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
TabBarPosition.bottom => BrowserBottomAppBar(
displayedSheet: displayedSheet,
showMainToolbar: showMainToolbar,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
},
);
}
final appBarVisible = useValueListenable(showAppBar);
final diffAcc = useRef(0.0);
void resetHiddenState() {
if (!ref.read(tabBarDismissableControllerProvider)) {
showAppBar.value = true;
displayAppBar.value = true;
}
diffAcc.value = 0.0;
@@ -137,12 +158,10 @@ class _TabBar extends HookConsumerWidget {
}
});
ref.listen(tabScrollYProvider(tabId, const Duration(milliseconds: 50)), (
previous,
next,
) {
if (previous?.value != null && next.value != null) {
final diff = previous!.value! - next.value!;
useOnStreamChange(
pointerMoveEvents,
onData: (event) {
final diff = event.dy;
if (diff < 0) {
if (diffAcc.value > 0) {
diffAcc.value = 0.0;
@@ -150,7 +169,7 @@ class _TabBar extends HookConsumerWidget {
diffAcc.value += diff;
if (diffAcc.value.abs() > kToolbarHeight * 1.5) {
showAppBar.value = false;
displayAppBar.value = false;
}
} else if (diff > 0) {
if (diffAcc.value < 0) {
@@ -162,18 +181,26 @@ class _TabBar extends HookConsumerWidget {
resetHiddenState();
}
}
}
});
},
);
return Visibility(
visible:
sheetController.value != null || (!tabInFullScreen && appBarVisible),
maintainState: true,
child: BrowserBottomAppBar(
displayedSheet: displayedSheet,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
child: switch (tabBarPosition) {
TabBarPosition.top => BrowserTopAppBar(
showMainToolbar: showMainToolbar,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
TabBarPosition.bottom => BrowserBottomAppBar(
showMainToolbar: showMainToolbar,
displayedSheet: displayedSheet,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
},
);
}
}
@@ -191,7 +218,13 @@ class BrowserScreen extends HookConsumerWidget {
final overlayController = useOverlayPortalController();
final showContextualAppBar = ref.watch(
final tabBarPosition = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.tabBarPosition,
),
);
final showContextualToolbar = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.tabBarShowContextualBar,
),
@@ -203,10 +236,14 @@ class BrowserScreen extends HookConsumerWidget {
),
);
final showAppBar = useValueNotifier(true);
final displayAppBar = useValueNotifier(true);
final removeTopAppBar = useState(false);
ref.listen(tabBarDismissableControllerProvider, (previous, next) {
showAppBar.value = !next;
displayAppBar.value = !next;
if (tabBarPosition == TabBarPosition.bottom) {
removeTopAppBar.value = next;
}
});
ref.listen(overlayControllerProvider, (previous, next) {
@@ -244,6 +281,8 @@ class BrowserScreen extends HookConsumerWidget {
final sheetController = useState<PersistentBottomSheetController?>(null);
final pointerMoveEventsController = useStreamController<Offset>();
return PopScope(
//We need this for BackButtonListener to work downstream
//No direct pop result will be handled here
@@ -256,23 +295,50 @@ class BrowserScreen extends HookConsumerWidget {
//This causes issues with a non dismissable barrier pushed, we ahve our own barrier and this does seem to have issues when dismissing, so disable it completely
return null;
},
body: Column(
children: [
_TabBar(
showAppBar: showAppBar,
sheetController: sheetController,
showContextualToolbar: showContextualAppBar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
Expanded(
child: _Browser(
overlayController: overlayController,
sheetController: sheetController,
showAppBar: showAppBar,
tabInFullScreen: tabInFullScreen,
),
),
],
appBar: (tabBarPosition == TabBarPosition.top)
? PreferredSize(
preferredSize: BrowserTopAppBar(
showMainToolbar: true,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
).preferredSize,
child: _TabBar(
tabBarPosition: TabBarPosition.top,
showMainToolbar: true,
displayAppBar: displayAppBar,
sheetController: sheetController,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
pointerMoveEvents: null,
),
)
: removeTopAppBar.value
? const PreferredSize(
preferredSize: Size.zero,
child: SizedBox.shrink(),
)
: null,
bottomNavigationBar: _TabBar(
tabBarPosition: TabBarPosition.bottom,
displayAppBar: displayAppBar,
sheetController: sheetController,
showMainToolbar: tabBarPosition == TabBarPosition.bottom,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
pointerMoveEvents: pointerMoveEventsController.stream,
),
body: _Browser(
overlayController: overlayController,
sheetController: sheetController,
displayAppBar: displayAppBar,
tabInFullScreen: tabInFullScreen,
pointerMoveEventSink: pointerMoveEventsController.sink,
bottomAppBarSize: BrowserBottomAppBar(
showMainToolbar: false,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
displayedSheet: null,
).preferredSize,
),
floatingActionButton: BrowserFab(),
),
@@ -286,15 +352,19 @@ class _Browser extends HookConsumerWidget {
final OverlayPortalController overlayController;
final ValueNotifier<PersistentBottomSheetController?> sheetController;
final ValueNotifier<bool> showAppBar;
final ValueNotifier<bool> displayAppBar;
final StreamSink<Offset> pointerMoveEventSink;
final Size bottomAppBarSize;
final bool tabInFullScreen;
const _Browser({
required this.overlayController,
required this.sheetController,
required this.showAppBar,
required this.displayAppBar,
required this.tabInFullScreen,
required this.pointerMoveEventSink,
required this.bottomAppBarSize,
});
@override
@@ -363,6 +433,7 @@ class _Browser extends HookConsumerWidget {
child: _ViewUrlSheet(
initialTabState: parameter.tabState,
maxChildSize: relativeSafeArea,
bottomAppBarSize: bottomAppBarSize,
),
),
};
@@ -453,7 +524,7 @@ class _Browser extends HookConsumerWidget {
//Make sure app bar is visible
if (!ref.read(tabBarDismissableControllerProvider)) {
showAppBar.value = true;
displayAppBar.value = true;
}
if (tabState?.isLoading == true) {
@@ -530,6 +601,7 @@ class _Browser extends HookConsumerWidget {
child: _BrowserView(
sheetDisplayed: sheetController.value != null,
isFullscreen: tabInFullScreen,
pointerMoveEventSink: pointerMoveEventSink,
),
),
),
@@ -542,10 +614,12 @@ class _Browser extends HookConsumerWidget {
class _BrowserView extends StatelessWidget {
final bool sheetDisplayed;
final bool isFullscreen;
final StreamSink<Offset>? pointerMoveEventSink;
const _BrowserView({
required this.sheetDisplayed,
required this.isFullscreen,
this.pointerMoveEventSink,
});
@override
@@ -559,7 +633,7 @@ class _BrowserView extends StatelessWidget {
left: !isFullscreen,
child: Stack(
children: [
const BrowserView(),
BrowserView(pointerMoveEventSink: pointerMoveEventSink),
Positioned(
bottom: 0,
left: 0,
@@ -627,8 +701,13 @@ class _BrowserView extends StatelessWidget {
class _ViewUrlSheet extends HookConsumerWidget {
final double maxChildSize;
final TabState initialTabState;
final Size bottomAppBarSize;
const _ViewUrlSheet({required this.initialTabState, this.maxChildSize = 1.0});
const _ViewUrlSheet({
required this.initialTabState,
required this.bottomAppBarSize,
this.maxChildSize = 1.0,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -666,6 +745,7 @@ class _ViewUrlSheet extends HookConsumerWidget {
}
},
initialHeight: initialHeight,
bottomAppBarHeight: bottomAppBarSize.height,
),
);
},
@@ -44,35 +44,114 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/providers.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/cached_future.dart';
import 'package:weblibre/presentation/hooks/menu_controller.dart';
import 'package:weblibre/presentation/icons/tor_icons.dart';
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
class BrowserBottomAppBar extends HookConsumerWidget {
class BrowserTopAppBar extends HookConsumerWidget {
final bool showMainToolbar;
final bool showContextualToolbar;
final bool showQuickTabSwitcherBar;
const BrowserBottomAppBar({
late final BrowserTabBar _tabBar;
late final _size = Size.fromHeight(_tabBar.getToolbarHeight());
BrowserTopAppBar({
required this.showMainToolbar,
required this.showContextualToolbar,
required this.showQuickTabSwitcherBar,
}) {
_tabBar = BrowserTabBar(
showMainToolbar: showMainToolbar,
displayedSheet: null,
showContextualToolbar: false,
showQuickTabSwitcherBar: false,
showMainToolbarNavigationButton: !showContextualToolbar,
showMainToolbarTabsCount: !showContextualToolbar,
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return SafeArea(
child: SizedBox(height: preferredSize.height, child: _tabBar),
);
}
Size get preferredSize => _size;
}
class BrowserBottomAppBar extends HookConsumerWidget {
final bool showMainToolbar;
final bool showContextualToolbar;
final bool showQuickTabSwitcherBar;
final Sheet? displayedSheet;
late final BrowserTabBar _tabBar;
late final _size = Size.fromHeight(_tabBar.getToolbarHeight());
BrowserBottomAppBar({
required this.showMainToolbar,
required this.displayedSheet,
required this.showContextualToolbar,
required this.showQuickTabSwitcherBar,
});
}) {
_tabBar = BrowserTabBar(
displayedSheet: displayedSheet,
showMainToolbar: showMainToolbar,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
showMainToolbarNavigationButton: !showContextualToolbar,
showMainToolbarTabsCount: !showContextualToolbar,
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return BottomAppBar(
height: _size.height,
padding: EdgeInsets.zero,
child: _tabBar,
);
}
Size get preferredSize => _size;
}
class BrowserTabBar extends HookConsumerWidget {
final bool showMainToolbar;
final bool showContextualToolbar;
final bool showQuickTabSwitcherBar;
final Sheet? displayedSheet;
final bool showMainToolbarTabsCount;
final bool showMainToolbarNavigationButton;
const BrowserTabBar({
required this.showMainToolbar,
required this.displayedSheet,
required this.showContextualToolbar,
required this.showQuickTabSwitcherBar,
required this.showMainToolbarTabsCount,
required this.showMainToolbarNavigationButton,
});
static const contextualToolabarHeight = 54.0;
static const quickTabSwitcherHeight = 48.0;
bool get displayAppBar =>
!showContextualToolbar || displayedSheet is! ViewTabsSheet;
showMainToolbar &&
(!showContextualToolbar || displayedSheet is! ViewTabsSheet);
bool get displayQuickTabSwitcher =>
showQuickTabSwitcherBar && displayedSheet is! ViewTabsSheet;
@@ -113,67 +192,68 @@ class BrowserBottomAppBar extends HookConsumerWidget {
final dragStartPosition = useRef(Offset.zero);
return BottomAppBar(
height: getToolbarHeight(),
padding: EdgeInsets.zero,
child: GestureDetector(
onTap: () {
if (displayedSheet case EditUrlSheet()) {
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
return;
}
final toolbarHeight = useMemoized(() => getToolbarHeight());
final tabState = ref.read(selectedTabStateProvider);
return GestureDetector(
onTap: () {
if (displayedSheet case EditUrlSheet()) {
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
return;
}
if (tabState != null) {
ref
.read(bottomSheetControllerProvider.notifier)
.show(EditUrlSheet(tabState: tabState));
}
},
onHorizontalDragStart: (details) {
dragStartPosition.value = details.globalPosition;
},
onHorizontalDragEnd: (details) async {
final distance = dragStartPosition.value - details.globalPosition;
final tabState = ref.read(selectedTabStateProvider);
if (distance.dx.abs() > 50 && distance.dy.abs() < 20) {
final selectedTab = ref.read(selectedTabProvider);
final setting = await ref
.read(generalSettingsRepositoryProvider.notifier)
.fetchSettings();
if (tabState != null) {
ref
.read(bottomSheetControllerProvider.notifier)
.show(EditUrlSheet(tabState: tabState));
}
},
onHorizontalDragStart: (details) {
dragStartPosition.value = details.globalPosition;
},
onHorizontalDragEnd: (details) async {
final distance = dragStartPosition.value - details.globalPosition;
if (selectedTab != null) {
switch (setting.tabBarSwipeAction) {
case TabBarSwipeAction.switchLastOpened:
if (distance.dx.abs() > 50 && distance.dy.abs() < 20) {
final selectedTab = ref.read(selectedTabProvider);
final setting = await ref
.read(generalSettingsRepositoryProvider.notifier)
.fetchSettings();
if (selectedTab != null) {
switch (setting.tabBarSwipeAction) {
case TabBarSwipeAction.switchLastOpened:
await ref
.read(tabRepositoryProvider.notifier)
.selectPreviouslyOpenedTab(selectedTab);
case TabBarSwipeAction.navigateOrderedTabs:
if (distance.dx < 0) {
await ref
.read(tabRepositoryProvider.notifier)
.selectPreviouslyOpenedTab(selectedTab);
case TabBarSwipeAction.navigateOrderedTabs:
if (distance.dx < 0) {
await ref
.read(tabRepositoryProvider.notifier)
.selectPreviousTab(selectedTab);
} else {
await ref
.read(tabRepositoryProvider.notifier)
.selectNextTab(selectedTab);
}
}
.selectPreviousTab(selectedTab);
} else {
await ref
.read(tabRepositoryProvider.notifier)
.selectNextTab(selectedTab);
}
}
} else if (distance.dy < 20 && distance.dx.abs() < 15) {
ref.read(tabBarDismissableControllerProvider.notifier).dismiss();
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (showQuickTabSwitcherBar)
Visibility(
visible: displayQuickTabSwitcher,
maintainState: true,
child: QuickTabSwitcher(),
),
} else if (distance.dy < (toolbarHeight / 3) &&
distance.dx.abs() < 15) {
ref.read(tabBarDismissableControllerProvider.notifier).dismiss();
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (showQuickTabSwitcherBar)
Visibility(
visible: displayQuickTabSwitcher,
maintainState: true,
child: QuickTabSwitcher(),
),
if (showMainToolbar)
Visibility(
visible: displayAppBar,
maintainState: true,
@@ -269,29 +349,40 @@ class BrowserBottomAppBar extends HookConsumerWidget {
),
),
),
if (!showContextualToolbar)
if (showMainToolbarTabsCount)
TabsCountButton(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
showLongPressMenu: true,
),
if (!showContextualToolbar)
NavigationMenuButton(selectedTabId: selectedTabId),
if (showMainToolbarNavigationButton)
NavigationMenuButton(
selectedTabId: selectedTabId,
showNavigationButtons: true,
),
],
),
),
if (showContextualToolbar)
ContextualToolbar(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
),
],
),
if (showContextualToolbar)
ContextualToolbar(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
),
],
),
);
}
}
typedef _QuickTabItem = ({
Color? color,
String id,
bool isPrivate,
bool isHistory,
String title,
Uri url,
});
class ContextualToolbar extends HookConsumerWidget {
const ContextualToolbar({
super.key,
@@ -333,7 +424,10 @@ class ContextualToolbar extends HookConsumerWidget {
displayedSheet: displayedSheet,
showLongPressMenu: false,
),
NavigationMenuButton(selectedTabId: selectedTabId),
NavigationMenuButton(
selectedTabId: selectedTabId,
showNavigationButtons: false,
),
],
);
}
@@ -342,7 +436,16 @@ class ContextualToolbar extends HookConsumerWidget {
class QuickTabSwitcher extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabStates = ref.watch(fifoTabStatesProvider).value;
final tabStates = ref.watch(fifoTabStatesProvider);
final historyAsync = useCachedFuture(() {
if (tabStates.value.length <= 1) {
return ref
.read(historyRepositoryProvider.notifier)
.getVisitsPaginated(count: 25);
}
return Future.value(<VisitInfo>[]);
}, [tabStates.value.length <= 1]);
final chipScrollController = useScrollController();
@@ -351,19 +454,19 @@ class QuickTabSwitcher extends HookConsumerWidget {
child: SizedBox(
height: 48,
width: double.maxFinite,
child: SelectableChips<FifoTab, FifoTab, String>(
child: SelectableChips<_QuickTabItem, _QuickTabItem, String>(
enableDelete: false,
scrollController: chipScrollController,
itemId: (item) => item.$1.id,
itemId: (item) => item.id,
itemLabel: (item) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 64),
child: Text(item.$1.titleOrAuthority),
child: Text(item.title),
),
if (item.$1.isPrivate)
if (item.isPrivate)
const Padding(
padding: EdgeInsets.only(left: 8.0),
child: Icon(
@@ -372,23 +475,57 @@ class QuickTabSwitcher extends HookConsumerWidget {
size: 20,
),
),
if (item.isHistory)
const Padding(
padding: EdgeInsets.only(left: 8.0),
child: Icon(MdiIcons.history, size: 20),
),
],
);
},
itemAvatar: (item) => UrlIcon([item.$1.url], iconSize: 16),
itemBackgroundColor: (item) => item.$2?.color.withValues(alpha: 0.5),
itemAvatar: (item) => UrlIcon([item.url], iconSize: 16),
itemBackgroundColor: (item) => item.color?.withValues(alpha: 0.5),
onSelected: (item) async {
final animation = chipScrollController.animateTo(
0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
);
await ref
.read(tabRepositoryProvider.notifier)
.selectTab(item.$1.id);
if (item.isHistory) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: item.url, private: false);
} else {
await ref.read(tabRepositoryProvider.notifier).selectTab(item.id);
}
await animation;
},
availableItems: tabStates.skip(1),
availableItems: tabStates.value
.map(
(state) => (
id: state.$1.id,
title: state.$1.titleOrAuthority,
isPrivate: state.$1.isPrivate,
isHistory: false,
url: state.$1.url,
color: state.$2?.color,
),
)
.skip(1)
.followedBy(
(historyAsync.data ?? []).map((state) {
final url = Uri.parse(state.url);
return (
id: state.url,
title: state.title ?? url.authority,
isPrivate: false,
isHistory: true,
url: url,
color: null,
);
}),
),
),
),
);
@@ -430,18 +567,18 @@ class ShareMenuButton extends HookConsumerWidget {
}
class NavigationMenuButton extends HookConsumerWidget {
const NavigationMenuButton({super.key, required this.selectedTabId});
final String? selectedTabId;
final bool showNavigationButtons;
const NavigationMenuButton({
super.key,
required this.selectedTabId,
required this.showNavigationButtons,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final addonService = ref.watch(addonServiceProvider);
final showContextualAppBar = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.tabBarShowContextualBar,
),
);
final hamburgerMenuController = useMenuController();
@@ -674,7 +811,7 @@ class NavigationMenuButton extends HookConsumerWidget {
child: const Text('Reload'),
),
if (selectedTabId != null) const Divider(),
if (selectedTabId != null && !showContextualAppBar)
if (selectedTabId != null && showNavigationButtons)
Consumer(
builder: (context, ref, child) {
final history = ref.watch(
@@ -44,6 +44,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/providers/li
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/services/engine_settings_replication.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart';
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
@@ -59,11 +60,13 @@ class BrowserView extends StatefulHookConsumerWidget {
final Duration screenshotPeriod;
final Duration suggestionTimeout;
final Future<void> Function()? postInitializationStep;
final StreamSink<Offset>? pointerMoveEventSink;
const BrowserView({
this.screenshotPeriod = const Duration(seconds: 10),
this.suggestionTimeout = const Duration(seconds: 30),
this.postInitializationStep,
this.pointerMoveEventSink,
});
@override
@@ -106,9 +109,9 @@ class _BrowserViewState extends ConsumerState<BrowserView>
if (settings.historyAutoCleanInterval > Duration.zero) {
await GeckoHistoryService().deleteVisitsBetween(
DateTime(0),
DateTime.now().subtract(settings.historyAutoCleanInterval),
);
DateTime(0),
DateTime.now().subtract(settings.historyAutoCleanInterval),
);
}
});
});
@@ -216,74 +219,87 @@ class _BrowserViewState extends ConsumerState<BrowserView>
},
);
return Stack(
children: [
GeckoView(
preInitializationStep: () async {
await ref
.read(eventServiceProvider)
.viewReadyStateEvents
.firstWhere((state) => state == true)
.timeout(
const Duration(seconds: 3),
onTimeout: () {
logger.e(
'Browser fragement not reported ready, trying to intitialize anyways',
);
return true;
},
);
},
postInitializationStep: () async {
await widget.postInitializationStep?.call();
if (!initializationCompleter.isCompleted) {
const quickActions = QuickActions();
//Debounce: https://github.com/flutter/flutter/issues/131121
DateTime? lastAction;
await quickActions.initialize((type) async {
if (lastAction == null ||
DateTime.now().difference(lastAction!) >
const Duration(seconds: 5)) {
if (type == 'new_tab') {
lastAction = DateTime.now();
final router = await ref.read(routerProvider.future);
const route = SearchRoute(tabType: TabType.regular);
await router.push(route.location);
} else if (type == 'new_private_tab') {
lastAction = DateTime.now();
final router = await ref.read(routerProvider.future);
const route = SearchRoute(tabType: TabType.private);
await router.push(route.location);
} else {
throw UnimplementedError();
}
}
});
await quickActions.setShortcutItems([
//TODO: add icons
const ShortcutItem(type: 'new_tab', localizedTitle: 'New Tab'),
const ShortcutItem(
type: 'new_private_tab',
localizedTitle: 'New Private Tab',
),
]);
initializationCompleter.complete();
return Listener(
behavior: HitTestBehavior.translucent,
onPointerMove: (widget.pointerMoveEventSink != null)
? (event) {
if (event.down) {
widget.pointerMoveEventSink!.add(event.localDelta);
}
}
},
),
if (!hasTab)
Positioned.fill(
child: SizedBox.expand(child: Container(color: Colors.grey[800])),
: null,
child: Stack(
children: [
GeckoView(
preInitializationStep: () async {
await ref
.read(eventServiceProvider)
.viewReadyStateEvents
.firstWhere((state) => state == true)
.timeout(
const Duration(seconds: 3),
onTimeout: () {
logger.e(
'Browser fragement not reported ready, trying to intitialize anyways',
);
return true;
},
);
},
postInitializationStep: () async {
await widget.postInitializationStep?.call();
if (!initializationCompleter.isCompleted) {
const quickActions = QuickActions();
//Debounce: https://github.com/flutter/flutter/issues/131121
DateTime? lastAction;
await quickActions.initialize((type) async {
if (lastAction == null ||
DateTime.now().difference(lastAction!) >
const Duration(seconds: 5)) {
if (type == 'new_tab') {
lastAction = DateTime.now();
final router = await ref.read(routerProvider.future);
const route = SearchRoute(tabType: TabType.regular);
await router.push(route.location);
} else if (type == 'new_private_tab') {
lastAction = DateTime.now();
final router = await ref.read(routerProvider.future);
const route = SearchRoute(tabType: TabType.private);
await router.push(route.location);
} else {
throw UnimplementedError();
}
}
});
await quickActions.setShortcutItems([
//TODO: add icons
const ShortcutItem(
type: 'new_tab',
localizedTitle: 'New Tab',
),
const ShortcutItem(
type: 'new_private_tab',
localizedTitle: 'New Private Tab',
),
]);
initializationCompleter.complete();
}
},
),
],
if (!hasTab)
Positioned.fill(
child: SizedBox.expand(child: Container(color: Colors.grey[800])),
),
],
),
);
}
@@ -48,6 +48,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
final DraggableScrollableController draggableScrollableController;
final VoidCallback onClose;
final double initialHeight;
final double bottomAppBarHeight;
const ViewTabSheetWidget({
required this.initialTabState,
@@ -55,6 +56,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
required this.draggableScrollableController,
required this.onClose,
required this.initialHeight,
required this.bottomAppBarHeight,
});
@override
@@ -79,7 +81,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
final totalHeight =
headerBox.size.height +
textBox.size.height +
kToolbarHeight +
bottomAppBarHeight +
MediaQuery.of(context).viewInsets.bottom;
final relative = (totalHeight / MediaQuery.of(context).size.height)
@@ -362,6 +362,55 @@ class GeneralSettingsScreen extends HookConsumerWidget {
);
},
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Tab Bar Position'),
leading: Icon(MdiIcons.dockWindow),
contentPadding: EdgeInsets.zero,
),
RadioGroup(
groupValue: generalSettings.tabBarPosition,
onChanged: (value) async {
if (value != null) {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) => currentSettings.copyWith
.tabBarPosition(value),
);
}
},
child: const Column(
children: [
RadioListTile.adaptive(
value: TabBarPosition.top,
title: Text('Top'),
subtitle: Text(
'Persistent tab bar without auto-hide',
),
),
RadioListTile.adaptive(
value: TabBarPosition.bottom,
title: Text('Bottom'),
subtitle: Text('Tab bar with auto-hide support'),
),
],
),
),
],
),
),
SwitchListTile.adaptive(
title: const Text('Show Contextual Tab Bar'),
subtitle: const Text(
@@ -38,6 +38,8 @@ enum TabBarSwipeAction { switchLastOpened, navigateOrderedTabs }
enum TabIntentOpenSetting { regular, private, ask }
enum TabBarPosition { top, bottom }
enum DeleteBrowsingDataType {
tabs('Open tabs'),
history('Browsing history'),
@@ -74,6 +76,7 @@ class GeneralSettings with FastEquatable {
final bool tabBarReaderView;
final bool tabBarShowContextualBar;
final bool tabBarShowQuickTabSwitcherBar;
final TabBarPosition tabBarPosition;
GeneralSettings({
required this.themeMode,
@@ -94,6 +97,7 @@ class GeneralSettings with FastEquatable {
required this.tabBarReaderView,
required this.tabBarShowContextualBar,
required this.tabBarShowQuickTabSwitcherBar,
required this.tabBarPosition,
});
GeneralSettings.withDefaults({
@@ -115,6 +119,7 @@ class GeneralSettings with FastEquatable {
bool? tabBarReaderView,
bool? tabBarShowContextualBar,
bool? tabBarShowQuickTabSwitcherBar,
TabBarPosition? tabBarPosition,
}) : themeMode = themeMode ?? ThemeMode.dark,
enableReadability = enableReadability ?? true,
enforceReadability = enforceReadability ?? false,
@@ -134,7 +139,8 @@ class GeneralSettings with FastEquatable {
tabViewBottomSheet = tabViewBottomSheet ?? false,
tabBarReaderView = tabBarReaderView ?? false,
tabBarShowContextualBar = tabBarShowContextualBar ?? true,
tabBarShowQuickTabSwitcherBar = tabBarShowQuickTabSwitcherBar ?? true;
tabBarShowQuickTabSwitcherBar = tabBarShowQuickTabSwitcherBar ?? true,
tabBarPosition = tabBarPosition ?? TabBarPosition.bottom;
factory GeneralSettings.fromJson(Map<String, dynamic> json) =>
_$GeneralSettingsFromJson(json);
@@ -161,5 +167,6 @@ class GeneralSettings with FastEquatable {
tabBarReaderView,
tabBarShowContextualBar,
tabBarShowQuickTabSwitcherBar,
tabBarPosition,
];
}
@@ -51,6 +51,8 @@ abstract class _$GeneralSettingsCWProxy {
bool tabBarShowQuickTabSwitcherBar,
);
GeneralSettings tabBarPosition(TabBarPosition tabBarPosition);
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
///
@@ -77,6 +79,7 @@ abstract class _$GeneralSettingsCWProxy {
bool tabBarReaderView,
bool tabBarShowContextualBar,
bool tabBarShowQuickTabSwitcherBar,
TabBarPosition tabBarPosition,
});
}
@@ -162,6 +165,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
bool tabBarShowQuickTabSwitcherBar,
) => call(tabBarShowQuickTabSwitcherBar: tabBarShowQuickTabSwitcherBar);
@override
GeneralSettings tabBarPosition(TabBarPosition tabBarPosition) =>
call(tabBarPosition: tabBarPosition);
@override
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
@@ -189,6 +196,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
Object? tabBarReaderView = const $CopyWithPlaceholder(),
Object? tabBarShowContextualBar = const $CopyWithPlaceholder(),
Object? tabBarShowQuickTabSwitcherBar = const $CopyWithPlaceholder(),
Object? tabBarPosition = const $CopyWithPlaceholder(),
}) {
return GeneralSettings(
themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null
@@ -295,6 +303,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
? _value.tabBarShowQuickTabSwitcherBar
// ignore: cast_nullable_to_non_nullable
: tabBarShowQuickTabSwitcherBar as bool,
tabBarPosition:
tabBarPosition == const $CopyWithPlaceholder() ||
tabBarPosition == null
? _value.tabBarPosition
// ignore: cast_nullable_to_non_nullable
: tabBarPosition as TabBarPosition,
);
}
}
@@ -351,6 +365,10 @@ GeneralSettings _$GeneralSettingsFromJson(
tabBarReaderView: json['tabBarReaderView'] as bool?,
tabBarShowContextualBar: json['tabBarShowContextualBar'] as bool?,
tabBarShowQuickTabSwitcherBar: json['tabBarShowQuickTabSwitcherBar'] as bool?,
tabBarPosition: $enumDecodeNullable(
_$TabBarPositionEnumMap,
json['tabBarPosition'],
),
);
Map<String, dynamic> _$GeneralSettingsToJson(
@@ -381,6 +399,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
'tabBarReaderView': instance.tabBarReaderView,
'tabBarShowContextualBar': instance.tabBarShowContextualBar,
'tabBarShowQuickTabSwitcherBar': instance.tabBarShowQuickTabSwitcherBar,
'tabBarPosition': _$TabBarPositionEnumMap[instance.tabBarPosition]!,
};
const _$ThemeModeEnumMap = {
@@ -422,3 +441,8 @@ const _$TabBarSwipeActionEnumMap = {
TabBarSwipeAction.switchLastOpened: 'switchLastOpened',
TabBarSwipeAction.navigateOrderedTabs: 'navigateOrderedTabs',
};
const _$TabBarPositionEnumMap = {
TabBarPosition.top: 'top',
TabBarPosition.bottom: 'bottom',
};
@@ -114,6 +114,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
),
'tabBarShowQuickTabSwitcherBar': settings['tabBarShowQuickTabSwitcherBar']
?.readAs(DriftSqlType.bool, db.typeMapping),
'tabBarPosition': settings['tabBarPosition']?.readAs(
DriftSqlType.string,
db.typeMapping,
),
});
}
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
}
String _$generalSettingsRepositoryHash() =>
r'4014986c9510d6d7210c5c16d814f6b018a1f93c';
r'09542387844db25334384af115c538348b0ad33f';
abstract class _$GeneralSettingsRepository
extends $StreamNotifier<GeneralSettings> {