long press history navigation

This commit is contained in:
Fabian Freund
2026-01-21 07:13:34 +01:00
parent 94fa5789f5
commit d6e02f9d02
5 changed files with 251 additions and 36 deletions
@@ -56,6 +56,10 @@ class TabSession extends _$TabSession {
return _sessionService.goForward();
}
Future<void> goToHistoryIndex({required int index}) {
return _sessionService.goToHistoryIndex(index: index);
}
Future<void> exitFullscreen() {
return _sessionService.exitFullscreen();
}
@@ -57,7 +57,7 @@ final class TabSessionProvider extends $NotifierProvider<TabSession, void> {
}
}
String _$tabSessionHash() => r'4b7206863e205dfeee6201ba6072e742c43ab6f3';
String _$tabSessionHash() => r'5be471574bb868889cc1eee37ae7e4570a9e21a4';
final class TabSessionFamily extends $Family
with $ClassFamilyOverride<TabSession, void, void, void, String?> {
@@ -41,6 +41,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/contro
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_badge_icon.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/history_menu.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart';
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';
@@ -924,18 +925,32 @@ class NavigateForwardButton extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
onPressed: canGoForward
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
final historyMenuController = useMenuController();
await controller.goForward();
menuControllerToClose?.close();
}
: null,
icon: const Icon(Icons.arrow_forward),
return HistoryMenu(
selectedTabId: selectedTabId,
controller: historyMenuController,
direction: HistoryMenuDirection.forward,
child: IconButton(
onPressed: canGoForward
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
await controller.goForward();
menuControllerToClose?.close();
}
: null,
onLongPress: canGoForward
? () {
if (!historyMenuController.isOpen) {
historyMenuController.open();
}
}
: null,
icon: const Icon(Icons.arrow_forward),
),
);
}
}
@@ -956,33 +971,47 @@ class NavigateBackButton extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
onPressed: (canGoBack || isLoading)
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
final historyMenuController = useMenuController();
final isReaderActive = ref.read(
selectedTabStateProvider.select(
(state) => state?.readerableState.active ?? false,
),
);
return HistoryMenu(
selectedTabId: selectedTabId,
controller: historyMenuController,
direction: HistoryMenuDirection.back,
child: IconButton(
onPressed: (canGoBack || isLoading)
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
if (isLoading) {
await controller.stopLoading();
} else if (isReaderActive) {
await ref
.read(readerableScreenControllerProvider.notifier)
.toggleReaderView(false);
} else {
await controller.goBack();
final isReaderActive = ref.read(
selectedTabStateProvider.select(
(state) => state?.readerableState.active ?? false,
),
);
if (isLoading) {
await controller.stopLoading();
} else if (isReaderActive) {
await ref
.read(readerableScreenControllerProvider.notifier)
.toggleReaderView(false);
} else {
await controller.goBack();
}
menuControllerToClose?.close();
}
menuControllerToClose?.close();
}
: null,
icon: isLoading ? const Icon(Icons.close) : const Icon(Icons.arrow_back),
: null,
onLongPress: (canGoBack && !isLoading)
? () {
if (!historyMenuController.isOpen) {
historyMenuController.open();
}
}
: null,
icon: isLoading ? const Icon(Icons.close) : const Icon(Icons.arrow_back),
),
);
}
}
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2024-2025 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:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/history.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/history_menu_item.dart';
enum HistoryMenuDirection { back, forward }
class HistoryMenu extends HookConsumerWidget {
const HistoryMenu({
super.key,
required this.selectedTabId,
required this.controller,
required this.direction,
required this.child,
});
final String? selectedTabId;
final MenuController controller;
final HistoryMenuDirection direction;
final Widget child;
@override
Widget build(BuildContext context, WidgetRef ref) {
final historyState = ref.watch(
tabStateProvider(selectedTabId).select((state) => state?.historyState),
);
final menuItems = useMemoized(() => _buildMenuItems(historyState), [
historyState,
]);
return MenuAnchor(
controller: controller,
consumeOutsideTap: true,
menuChildren: menuItems,
child: child,
);
}
List<Widget> _buildMenuItems(HistoryState? historyState) {
if (historyState == null || historyState.items.isEmpty) {
return [
MenuItemButton(
child: Text(
direction == HistoryMenuDirection.back
? 'No previous pages'
: 'No forward pages',
),
),
];
}
final items = historyState.items.indexed;
final currentIndex = historyState.currentIndex;
final historyItems = switch (direction) {
// Back: oldest at top, newest at bottom (close to button)
HistoryMenuDirection.back => items.where((e) => e.$1 < currentIndex),
// Forward: furthest at top, closest at bottom (close to button)
HistoryMenuDirection.forward =>
items.where((e) => e.$1 > currentIndex).toList().reversed,
};
if (historyItems.isEmpty) {
final message = direction == HistoryMenuDirection.back
? 'No previous pages'
: 'No forward pages';
return [MenuItemButton(child: Text(message))];
}
return historyItems
.map(
(e) => HistoryMenuItem(
selectedTabId: selectedTabId,
item: e.$2,
historyIndex: e.$1,
),
)
.toList();
}
}
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2024-2025 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/domain/entities/states/history.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
class HistoryMenuItem extends HookConsumerWidget {
const HistoryMenuItem({
super.key,
required this.selectedTabId,
required this.item,
required this.historyIndex,
});
final String? selectedTabId;
final HistoryItem item;
final int historyIndex;
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return MenuItemButton(
leadingIcon: UrlIcon([item.url], iconSize: 24),
closeOnActivate: false,
onPressed: () async {
await ref
.read(tabSessionProvider(tabId: selectedTabId).notifier)
.goToHistoryIndex(index: historyIndex);
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
},
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.title.isNotEmpty ? item.title : item.url.authority,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
UriBreadcrumb(
uri: item.url,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
}