long press history navigation
This commit is contained in:
@@ -56,6 +56,10 @@ class TabSession extends _$TabSession {
|
|||||||
return _sessionService.goForward();
|
return _sessionService.goForward();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> goToHistoryIndex({required int index}) {
|
||||||
|
return _sessionService.goToHistoryIndex(index: index);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> exitFullscreen() {
|
Future<void> exitFullscreen() {
|
||||||
return _sessionService.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
|
final class TabSessionFamily extends $Family
|
||||||
with $ClassFamilyOverride<TabSession, void, void, void, String?> {
|
with $ClassFamilyOverride<TabSession, void, void, void, String?> {
|
||||||
|
|||||||
+64
-35
@@ -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/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_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/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/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_creation_menu.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart';
|
||||||
@@ -924,18 +925,32 @@ class NavigateForwardButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return IconButton(
|
final historyMenuController = useMenuController();
|
||||||
onPressed: canGoForward
|
|
||||||
? () async {
|
|
||||||
final controller = ref.read(
|
|
||||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
|
||||||
);
|
|
||||||
|
|
||||||
await controller.goForward();
|
return HistoryMenu(
|
||||||
menuControllerToClose?.close();
|
selectedTabId: selectedTabId,
|
||||||
}
|
controller: historyMenuController,
|
||||||
: null,
|
direction: HistoryMenuDirection.forward,
|
||||||
icon: const Icon(Icons.arrow_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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return IconButton(
|
final historyMenuController = useMenuController();
|
||||||
onPressed: (canGoBack || isLoading)
|
|
||||||
? () async {
|
|
||||||
final controller = ref.read(
|
|
||||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
|
||||||
);
|
|
||||||
|
|
||||||
final isReaderActive = ref.read(
|
return HistoryMenu(
|
||||||
selectedTabStateProvider.select(
|
selectedTabId: selectedTabId,
|
||||||
(state) => state?.readerableState.active ?? false,
|
controller: historyMenuController,
|
||||||
),
|
direction: HistoryMenuDirection.back,
|
||||||
);
|
child: IconButton(
|
||||||
|
onPressed: (canGoBack || isLoading)
|
||||||
|
? () async {
|
||||||
|
final controller = ref.read(
|
||||||
|
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
final isReaderActive = ref.read(
|
||||||
await controller.stopLoading();
|
selectedTabStateProvider.select(
|
||||||
} else if (isReaderActive) {
|
(state) => state?.readerableState.active ?? false,
|
||||||
await ref
|
),
|
||||||
.read(readerableScreenControllerProvider.notifier)
|
);
|
||||||
.toggleReaderView(false);
|
|
||||||
} else {
|
if (isLoading) {
|
||||||
await controller.goBack();
|
await controller.stopLoading();
|
||||||
|
} else if (isReaderActive) {
|
||||||
|
await ref
|
||||||
|
.read(readerableScreenControllerProvider.notifier)
|
||||||
|
.toggleReaderView(false);
|
||||||
|
} else {
|
||||||
|
await controller.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
menuControllerToClose?.close();
|
||||||
}
|
}
|
||||||
|
: null,
|
||||||
menuControllerToClose?.close();
|
onLongPress: (canGoBack && !isLoading)
|
||||||
}
|
? () {
|
||||||
: null,
|
if (!historyMenuController.isOpen) {
|
||||||
icon: isLoading ? const Icon(Icons.close) : const Icon(Icons.arrow_back),
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user