improve the users bottom sheet and button placements

This commit is contained in:
Fabian Freund
2026-02-01 09:11:20 +01:00
parent c62c4f60a4
commit 0c2923fd82
4 changed files with 320 additions and 221 deletions
@@ -31,7 +31,6 @@ 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/providers.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
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/providers/web_extensions_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
@@ -41,8 +40,8 @@ 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/navigation_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';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
@@ -54,6 +53,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.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/presentation/dialogs/quit_browser_dialog.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';
@@ -61,6 +61,7 @@ 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/exit_app.dart';
class BrowserTopAppBar extends HookConsumerWidget {
final bool showMainToolbar;
@@ -372,10 +373,7 @@ class BrowserTabBar extends HookConsumerWidget {
showLongPressMenu: true,
),
if (showMainToolbarNavigationButton)
NavigationMenuButton(
selectedTabId: selectedTabId,
showNavigationButtons: true,
),
NavigationMenuButton(selectedTabId: selectedTabId),
],
),
),
@@ -441,10 +439,7 @@ class ContextualToolbar extends HookConsumerWidget {
displayedSheet: displayedSheet,
showLongPressMenu: false,
),
NavigationMenuButton(
selectedTabId: selectedTabId,
showNavigationButtons: false,
),
NavigationMenuButton(selectedTabId: selectedTabId),
],
);
}
@@ -623,13 +618,8 @@ class ShareMenuButton extends HookConsumerWidget {
class NavigationMenuButton extends HookConsumerWidget {
final String? selectedTabId;
final bool showNavigationButtons;
const NavigationMenuButton({
super.key,
required this.selectedTabId,
required this.showNavigationButtons,
});
const NavigationMenuButton({super.key, required this.selectedTabId});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -670,6 +660,17 @@ class NavigationMenuButton extends HookConsumerWidget {
},
),
),
MenuItemButton(
onPressed: () async {
final result = await showQuitBrowserDialog(context);
if (result == true && context.mounted) {
await exitApp(ProviderScope.containerOf(context));
}
},
leadingIcon: const Icon(MdiIcons.power),
child: const Text('Quit Browser'),
),
const Divider(),
Consumer(
builder: (context, childRef, child) {
@@ -854,171 +855,11 @@ class NavigationMenuButton extends HookConsumerWidget {
leadingIcon: const Icon(Icons.rss_feed),
child: const Text('Feeds'),
),
const Divider(),
if (selectedTabId != null)
MenuItemButton(
onPressed: () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
await controller.reload();
hamburgerMenuController.close();
},
leadingIcon: const Icon(Icons.refresh),
child: const Text('Reload'),
),
if (selectedTabId != null && showNavigationButtons) ...[
const Divider(),
Consumer(
builder: (context, ref, child) {
final history = ref.watch(
tabStateProvider(
selectedTabId,
).select((value) => value?.historyState),
);
final isLoading = ref.watch(
selectedTabStateProvider.select(
(state) => state?.isLoading ?? false,
),
);
return Row(
children: [
Expanded(
child: NavigateBackButton(
selectedTabId: selectedTabId,
isLoading: isLoading,
menuControllerToClose: hamburgerMenuController,
canGoBack: history?.canGoBack == true,
),
),
const SizedBox(height: 48, child: VerticalDivider()),
Expanded(
child: NavigateForwardButton(
selectedTabId: selectedTabId,
menuControllerToClose: hamburgerMenuController,
canGoForward: history?.canGoForward == true,
),
),
],
);
},
),
],
],
);
}
}
class NavigateForwardButton extends HookConsumerWidget {
const NavigateForwardButton({
super.key,
required this.selectedTabId,
this.menuControllerToClose,
this.canGoForward = true,
});
final String? selectedTabId;
final MenuController? menuControllerToClose;
final bool canGoForward;
@override
Widget build(BuildContext context, WidgetRef ref) {
final historyMenuController = useMenuController();
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),
),
);
}
}
class NavigateBackButton extends HookConsumerWidget {
const NavigateBackButton({
super.key,
required this.selectedTabId,
required this.isLoading,
this.menuControllerToClose,
this.canGoBack = true,
});
final String? selectedTabId;
final bool isLoading;
final MenuController? menuControllerToClose;
final bool canGoBack;
@override
Widget build(BuildContext context, WidgetRef ref) {
final historyMenuController = useMenuController();
return HistoryMenu(
selectedTabId: selectedTabId,
controller: historyMenuController,
direction: HistoryMenuDirection.back,
child: IconButton(
onPressed: (canGoBack || isLoading)
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
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();
}
: null,
onLongPress: (canGoBack && !isLoading)
? () {
if (!historyMenuController.isOpen) {
historyMenuController.open();
}
}
: null,
icon: isLoading
? const Icon(Icons.close)
: const Icon(Icons.arrow_back),
),
);
}
}
class AddTabButton extends HookConsumerWidget {
const AddTabButton();
@@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
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/features/browser/presentation/widgets/history_menu.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
import 'package:weblibre/presentation/hooks/menu_controller.dart';
class NavigateForwardButton extends HookConsumerWidget {
const NavigateForwardButton({
super.key,
required this.selectedTabId,
this.menuControllerToClose,
this.canGoForward = true,
});
final String? selectedTabId;
final MenuController? menuControllerToClose;
final bool canGoForward;
@override
Widget build(BuildContext context, WidgetRef ref) {
final historyMenuController = useMenuController();
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),
),
);
}
}
class NavigateBackButton extends HookConsumerWidget {
const NavigateBackButton({
super.key,
required this.selectedTabId,
required this.isLoading,
this.menuControllerToClose,
this.canGoBack = true,
});
final String? selectedTabId;
final bool isLoading;
final MenuController? menuControllerToClose;
final bool canGoBack;
@override
Widget build(BuildContext context, WidgetRef ref) {
final historyMenuController = useMenuController();
return HistoryMenu(
selectedTabId: selectedTabId,
controller: historyMenuController,
direction: HistoryMenuDirection.back,
child: IconButton(
onPressed: (canGoBack || isLoading)
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
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();
}
: null,
onLongPress: (canGoBack && !isLoading)
? () {
if (!historyMenuController.isOpen) {
historyMenuController.open();
}
}
: null,
icon: isLoading
? const Icon(Icons.close)
: const Icon(Icons.arrow_back),
),
);
}
}
@@ -31,9 +31,11 @@ import 'package:nullability/nullability.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:weblibre/features/geckoview/domain/providers/desktop_mode.dart';
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/presentation/widgets/menu_item_buttons.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
@@ -451,6 +453,55 @@ class TabMenu extends HookConsumerWidget {
leadingIcon: const Icon(MdiIcons.tabMinus),
child: const Text('Close Tab'),
),
const Divider(),
MenuItemButton(
onPressed: () async {
final sessionController = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
await sessionController.reload();
controller.close();
},
leadingIcon: const Icon(Icons.refresh),
child: const Text('Reload'),
),
Consumer(
builder: (context, ref, child) {
final history = ref.watch(
tabStateProvider(
selectedTabId,
).select((value) => value?.historyState),
);
final isLoading = ref.watch(
selectedTabStateProvider.select(
(state) => state?.isLoading ?? false,
),
);
return Row(
children: [
Expanded(
child: NavigateBackButton(
selectedTabId: selectedTabId,
isLoading: isLoading,
menuControllerToClose: controller,
canGoBack: history?.canGoBack == true,
),
),
const SizedBox(height: 48, child: VerticalDivider()),
Expanded(
child: NavigateForwardButton(
selectedTabId: selectedTabId,
menuControllerToClose: controller,
canGoForward: history?.canGoForward == true,
),
),
],
);
},
),
],
);
}
@@ -17,16 +17,17 @@
* 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 'dart:convert';
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/core/filesystem.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/user/domain/presentation/dialogs/quit_browser_dialog.dart';
import 'package:weblibre/domain/entities/profile.dart';
import 'package:weblibre/features/user/domain/presentation/utils/profile_switch_handler.dart';
import 'package:weblibre/features/user/domain/repositories/profile.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
import 'package:weblibre/utils/exit_app.dart';
/// Bottom sheet widget to select a user profile.
class SelectProfileDialog extends HookConsumerWidget {
@@ -44,30 +45,38 @@ class SelectProfileDialog extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Users',
'Select user',
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
const SizedBox(height: 24),
usersAsync.when(
skipLoadingOnReload: true,
data: (profiles) => Column(
mainAxisSize: MainAxisSize.min,
children: profiles.map((profile) {
final isSelected =
filesystem.selectedProfile == profile.uuidValue;
return ListTile(
key: ValueKey(profile.id),
enabled: !isSelected,
trailing:
!isSelected ? const Icon(MdiIcons.accountSwitch) : null,
title: Text(profile.name),
subtitle: isSelected ? const Text('Active') : null,
data: (profiles) => Wrap(
alignment: WrapAlignment.center,
spacing: 24,
runSpacing: 16,
children: [
...profiles.map(
(profile) => _ProfileAvatar(
profile: profile,
isActive: filesystem.selectedProfile == profile.uuidValue,
onTap: () async {
await handleSwitchProfile(context, ref, profile);
},
onLongPress: () async {
await EditProfileRoute(
profile: jsonEncode(profile.toJson()),
).push(context);
},
),
),
_AddProfileAvatar(
onTap: () async {
await handleSwitchProfile(context, ref, profile);
await CreateProfileRoute().push(context);
},
);
}).toList(),
),
],
),
error: (error, stackTrace) => Center(
child: FailureWidget(
@@ -77,31 +86,13 @@ class SelectProfileDialog extends HookConsumerWidget {
),
loading: () => const Center(child: CircularProgressIndicator()),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton.icon(
icon: const Icon(MdiIcons.power),
iconAlignment: IconAlignment.start,
label: const Text('Quit Browser'),
onPressed: () async {
final result = await showQuitBrowserDialog(context);
if (result == true) {
await exitApp(ref.container);
}
},
),
TextButton.icon(
icon: const Icon(MdiIcons.accountGroup),
iconAlignment: IconAlignment.end,
label: const Text('Manage'),
onPressed: () async {
await ProfileListRoute().push(context);
},
),
],
const SizedBox(height: 24),
TextButton.icon(
onPressed: () async {
await ProfileListRoute().push(context);
},
icon: const Icon(MdiIcons.accountGroup),
label: const Text('Manage Profiles'),
),
],
),
@@ -109,3 +100,105 @@ class SelectProfileDialog extends HookConsumerWidget {
);
}
}
class _ProfileAvatar extends StatelessWidget {
final Profile profile;
final bool isActive;
final VoidCallback onTap;
final VoidCallback onLongPress;
const _ProfileAvatar({
required this.profile,
required this.isActive,
required this.onTap,
required this.onLongPress,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
onLongPress: onLongPress,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 28,
backgroundColor: isActive
? colorScheme.primary
: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.person,
size: 24,
color: isActive
? colorScheme.onPrimary
: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
SizedBox(
width: 72,
child: Text(
profile.name,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
class _AddProfileAvatar extends StatelessWidget {
final VoidCallback onTap;
const _AddProfileAvatar({required this.onTap});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 28,
backgroundColor: Colors.transparent,
foregroundColor: colorScheme.onSurfaceVariant,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: colorScheme.outline),
),
child: Center(
child: Icon(
Icons.add,
size: 24,
color: colorScheme.onSurfaceVariant,
),
),
),
),
const SizedBox(height: 8),
SizedBox(
width: 72,
child: Text(
'Add user',
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}