diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index 8bece344..702f96b6 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -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(); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart new file mode 100644 index 00000000..f866bc6c --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart @@ -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), + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart index 2c2a22b3..1048cc65 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart @@ -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, + ), + ), + ], + ); + }, + ), ], ); } diff --git a/app/lib/features/user/domain/presentation/dialogs/select_profile.dart b/app/lib/features/user/domain/presentation/dialogs/select_profile.dart index 9e589410..d4dc8ee6 100644 --- a/app/lib/features/user/domain/presentation/dialogs/select_profile.dart +++ b/app/lib/features/user/domain/presentation/dialogs/select_profile.dart @@ -17,16 +17,17 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +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, + ), + ), + ], + ), + ); + } +}