diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart index 534f9e52..891b08da 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart @@ -1,108 +1,97 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:text_scroll/text_scroll.dart'; -import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; +import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart'; -class AppBarTitle extends StatelessWidget { - final TabState tab; - - final VoidCallback? onTap; - final VoidCallback? onLongPress; - final GestureDragStartCallback? onHorizontalDragStart; - final GestureDragEndCallback? onHorizontalDragEnd; - - const AppBarTitle({ - required this.tab, - this.onTap, - this.onLongPress, - this.onHorizontalDragStart, - this.onHorizontalDragEnd, - super.key, - }); - - Icon _securityStatusIcon(BuildContext context) { - if (tab.url.isScheme('http')) { - return Icon( - MdiIcons.lockOff, - color: Theme.of(context).colorScheme.error, - size: 14, - ); - } else if (!tab.securityInfoState.secure) { - return Icon( - MdiIcons.lockAlert, - color: Theme.of(context).colorScheme.errorContainer, - size: 14, - ); - } else if (!tab.isLoading) { - return const Icon(MdiIcons.lock, size: 14); - } else { - return const Icon(MdiIcons.timerSand, size: 14); - } - } +class AppBarTitle extends HookConsumerWidget { + const AppBarTitle({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - return GestureDetector( - onTap: onTap, - onLongPress: onLongPress, - onHorizontalDragStart: onHorizontalDragStart, - onHorizontalDragEnd: onHorizontalDragEnd, - child: Row( - children: [ - TabIcon(state: tab), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Skeletonizer( - enabled: tab.title.isEmpty, - child: Skeleton.replace( - replacement: const Padding( - padding: EdgeInsets.only(right: 4, top: 1, bottom: 1), - child: Bone.text(), - ), - child: TextScroll( - key: ValueKey(tab.title), - tab.title, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSurface, - ), - // mode: TextScrollMode.bouncing, - velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), - delayBefore: const Duration(milliseconds: 500), - pauseBetween: const Duration(milliseconds: 5000), - fadedBorder: true, - fadeBorderSide: FadeBorderSide.right, - fadedBorderWidth: 0.05, - intervalSpaces: 4, - numberOfReps: 2, + + final tabState = ref.watch(selectedTabStateProvider); + + if (tabState == null) { + return const SizedBox.shrink(); + } + + final icon = useMemoized(() { + if (tabState.url.isScheme('http')) { + return Icon( + MdiIcons.lockOff, + color: Theme.of(context).colorScheme.error, + size: 14, + ); + } else if (!tabState.securityInfoState.secure) { + return Icon( + MdiIcons.lockAlert, + color: Theme.of(context).colorScheme.errorContainer, + size: 14, + ); + } else if (!tabState.isLoading) { + return const Icon(MdiIcons.lock, size: 14); + } else { + return const Icon(MdiIcons.timerSand, size: 14); + } + }, [tabState]); + + return Row( + children: [ + TabIcon(state: tabState), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Skeletonizer( + enabled: tabState.title.isEmpty, + child: Skeleton.replace( + replacement: const Padding( + padding: EdgeInsets.only(right: 4, top: 1, bottom: 1), + child: Bone.text(), + ), + child: TextScroll( + key: ValueKey(tabState.title), + tabState.title, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface, ), + // mode: TextScrollMode.bouncing, + velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), + delayBefore: const Duration(milliseconds: 500), + pauseBetween: const Duration(milliseconds: 5000), + fadedBorder: true, + fadeBorderSide: FadeBorderSide.right, + fadedBorderWidth: 0.05, + intervalSpaces: 4, + numberOfReps: 2, ), ), - Row( - children: [ - _securityStatusIcon(context), - const SizedBox(width: 4), - Expanded( - child: Text( - tab.url.authority, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface, - ), + ), + Row( + children: [ + icon, + const SizedBox(width: 4), + Expanded( + child: Text( + tabState.url.authority, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, ), ), - ], - ), - ], - ), + ), + ], + ), + ], ), - ], - ), + ), + ], ); } } 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 e583ae06..ff55aa82 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 @@ -41,375 +41,373 @@ class BrowserBottomAppBar extends HookConsumerWidget { selectedTabStateProvider.select((state) => state?.isPrivate ?? false), ); + final dragStartPosition = useRef(Offset.zero); + return BottomAppBar( height: AppBar().preferredSize.height, padding: EdgeInsets.zero, - child: AppBar( - automaticallyImplyLeading: false, - titleSpacing: 8.0, - backgroundColor: - (isPrivateTab && displayedSheet is! ViewTabsSheet) - ? const Color(0x648000D7) - : null, - title: - (selectedTabId != null && displayedSheet is! ViewTabsSheet) - ? HookConsumer( - builder: (context, ref, child) { - final tabState = ref.watch(selectedTabStateProvider); + child: GestureDetector( + onTap: () async { + final tabState = ref.read(selectedTabStateProvider); - final dragStartPosition = useRef(Offset.zero); + if (tabState != null) { + await WebPageRoute( + url: tabState.url.toString(), + $extra: tabState, + ).push(context); + } + }, + onLongPress: () async { + final tabState = ref.read(selectedTabStateProvider); - return (tabState != null) - ? AppBarTitle( - tab: tabState, - onTap: () async { - await WebPageRoute( - url: tabState.url.toString(), - $extra: tabState, - ).push(context); - }, - onLongPress: () async { - final newUrl = await showDialog( - context: context, - builder: - (context) => - EditUrlDialog(initialUrl: tabState.url), - ); + if (tabState != null) { + final newUrl = await showDialog( + context: context, + builder: (context) => EditUrlDialog(initialUrl: tabState.url), + ); - if (newUrl != null) { - await ref - .read( - tabSessionProvider(tabId: null).notifier, - ) - .loadUrl(url: newUrl); - } - }, - onHorizontalDragStart: (details) { - dragStartPosition.value = details.globalPosition; - }, - onHorizontalDragEnd: (details) async { - final distance = - dragStartPosition.value - - details.globalPosition; + if (newUrl != null) { + await ref + .read(tabSessionProvider(tabId: null).notifier) + .loadUrl(url: newUrl); + } + } + }, + onHorizontalDragStart: (details) { + dragStartPosition.value = details.globalPosition; + }, + onHorizontalDragEnd: (details) async { + final distance = dragStartPosition.value - details.globalPosition; - if (distance.dx.abs() > 50) { - await ref - .read(tabRepositoryProvider.notifier) - .selectPreviousTab(); - } - }, - ) - : const SizedBox.shrink(); - }, - ) - : null, - actions: [ - if (selectedTabId != null && displayedSheet is! ViewTabsSheet) - ReaderButton(), - MenuAnchor( - controller: tabMenuController, - builder: (context, controller, child) { - return child!; - }, - menuChildren: [ - if (selectedTabId != null) ...[ - MenuItemButton( - onPressed: () async { - await ref - .read(tabRepositoryProvider.notifier) - .closeTab(selectedTabId); - }, - leadingIcon: const Icon(Icons.close), - child: const Text('Close Tab'), - ), - const Divider(), - ], - MenuItemButton( - onPressed: () async { - await const SearchRoute( - tabType: TabType.private, - ).push(context); - }, - leadingIcon: const Icon(MdiIcons.tabUnselected), - child: const Text('Add Private Tab'), - ), - MenuItemButton( - onPressed: () async { - await const SearchRoute( - tabType: TabType.regular, - ).push(context); - }, - leadingIcon: const Icon(MdiIcons.tabPlus), - child: const Text('Add Tab'), - ), - ], - child: TabsActionButton( - isActive: displayedSheet is ViewTabsSheet, - onTap: () { - if (displayedSheet case ViewTabsSheet()) { - ref.read(bottomSheetControllerProvider.notifier).dismiss(); - } else { - ref - .read(bottomSheetControllerProvider.notifier) - .show(ViewTabsSheet()); - } + if (distance.dx.abs() > 50) { + await ref.read(tabRepositoryProvider.notifier).selectPreviousTab(); + } + }, + child: AppBar( + automaticallyImplyLeading: false, + titleSpacing: 8.0, + backgroundColor: + (isPrivateTab && displayedSheet is! ViewTabsSheet) + ? const Color(0x648000D7) + : null, + title: + (selectedTabId != null && displayedSheet is! ViewTabsSheet) + ? const AppBarTitle() + : null, + actions: [ + if (selectedTabId != null && displayedSheet is! ViewTabsSheet) + ReaderButton(), + MenuAnchor( + controller: tabMenuController, + builder: (context, controller, child) { + return child!; }, - onLongPress: () { - if (tabMenuController.isOpen) { - tabMenuController.close(); - } else { - tabMenuController.open(); - } - }, - ), - ), - MenuAnchor( - controller: trippleDotMenuController, - builder: (context, controller, child) { - return Padding( - padding: const EdgeInsets.only(right: 4.0), - child: InkWell( - onTap: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: const Padding( - padding: EdgeInsets.symmetric( - vertical: 15.0, - horizontal: 8.0, - ), - child: Icon(Icons.more_vert), + menuChildren: [ + if (selectedTabId != null) ...[ + MenuItemButton( + onPressed: () async { + await ref + .read(tabRepositoryProvider.notifier) + .closeTab(selectedTabId); + }, + leadingIcon: const Icon(Icons.close), + child: const Text('Close Tab'), ), - ), - ); - }, - menuChildren: [ - Consumer( - builder: (context, childRef, child) { - final pageExtensions = childRef.watch( - webExtensionsStateProvider( - WebExtensionActionType.page, - ).select((value) => value.values.toList()), - ); - - return Wrap( - alignment: WrapAlignment.center, - children: [ - ...pageExtensions.map( - (extension) => IconButton( - onPressed: () async { - //Use parents .ref because after onPressed this consumer gets disposed already - await addonService.invokeAddonAction( - extension.extensionId, - WebExtensionActionType.page, - ); - }, - icon: ExtensionBadgeIcon(extension), - ), - ), - ], - ); - }, - ), - MenuItemButton( - onPressed: () async { - await AboutRoute().push(context); - }, - leadingIcon: const Icon(Icons.info), - child: const Text('About'), - ), - const Divider(), - MenuItemButton( - onPressed: () async { - await SettingsRoute().push(context); - }, - leadingIcon: const Icon(Icons.settings), - child: const Text('Settings'), - ), - Consumer( - builder: (context, ref, child) { - final browserExtensions = ref.watch( - webExtensionsStateProvider( - WebExtensionActionType.browser, - ).select((value) => value.values.toList()), - ); - - return SubmenuButton( - menuChildren: [ - ...browserExtensions.map( - (extension) => MenuItemButton( - onPressed: () async { - //Use parents .ref because after onPressed this consumer gets disposed already - await addonService.invokeAddonAction( - extension.extensionId, - WebExtensionActionType.browser, - ); - }, - leadingIcon: ExtensionBadgeIcon(extension), - child: Text(extension.title ?? ''), - ), - ), - MenuItemButton( - onPressed: () async { - await addonService.startAddonManagerActivity(); - }, - leadingIcon: const Icon(MdiIcons.puzzleEdit), - child: const Text('Addon Manager'), - ), - ], - leadingIcon: const Icon(MdiIcons.puzzle), - child: const Text('Addons'), - ); - }, - ), - MenuItemButton( - onPressed: () async { - await TorProxyRoute().push(context); - }, - leadingIcon: const Icon(TorIcons.onionAlt), - child: const Text('Tor'), - ), - const Divider(), - MenuItemButton( - onPressed: () async { - await BangCategoriesRoute().push(context); - }, - leadingIcon: const Icon(MdiIcons.exclamationThick), - child: const Text('Bangs'), - ), - MenuItemButton( - onPressed: () async { - await context.push(FeedListRoute().location); - }, - leadingIcon: const Icon(Icons.rss_feed), - child: const Text('Feeds'), - ), - const Divider(), - if (selectedTabId != null) + const Divider(), + ], MenuItemButton( onPressed: () async { - final tabState = ref.read(tabStateProvider(selectedTabId)); - - if (tabState?.url case final Uri url) { - await ui_helper.launchUrlFeedback(context, url); - } + await const SearchRoute( + tabType: TabType.private, + ).push(context); }, - leadingIcon: const Icon(Icons.open_in_browser), - child: const Text('Launch External'), + leadingIcon: const Icon(MdiIcons.tabUnselected), + child: const Text('Add Private Tab'), ), - if (selectedTabId != null) MenuItemButton( onPressed: () async { - final tabState = ref.read(tabStateProvider(selectedTabId)); - - if (tabState?.url case final Uri url) { - await SharePlus.instance.share(ShareParams(uri: url)); - } + await const SearchRoute( + tabType: TabType.regular, + ).push(context); }, - leadingIcon: const Icon(Icons.share), - child: const Text('Share'), + leadingIcon: const Icon(MdiIcons.tabPlus), + child: const Text('Add Tab'), ), - if (selectedTabId != null) const Divider(), - if (selectedTabId != null) - MenuItemButton( - onPressed: () { - ref.read(findInPageControllerProvider.notifier).show(); - }, - leadingIcon: const Icon(Icons.search), - child: const Text('Find in page'), - ), - if (selectedTabId != null) const Divider(), - if (selectedTabId != null) - MenuItemButton( - onPressed: () async { - final controller = ref.read( - tabSessionProvider(tabId: selectedTabId).notifier, - ); - - await controller.reload(); - trippleDotMenuController.close(); - }, - leadingIcon: const Icon(Icons.refresh), - child: const Text('Reload'), - ), - if (selectedTabId != null) const Divider(), - if (selectedTabId != null) + ], + child: TabsActionButton( + isActive: displayedSheet is ViewTabsSheet, + onTap: () { + if (displayedSheet case ViewTabsSheet()) { + ref.read(bottomSheetControllerProvider.notifier).dismiss(); + } else { + ref + .read(bottomSheetControllerProvider.notifier) + .show(ViewTabsSheet()); + } + }, + onLongPress: () { + if (tabMenuController.isOpen) { + tabMenuController.close(); + } else { + tabMenuController.open(); + } + }, + ), + ), + MenuAnchor( + controller: trippleDotMenuController, + builder: (context, controller, child) { + return Padding( + padding: const EdgeInsets.only(right: 4.0), + child: InkWell( + onTap: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Padding( + padding: EdgeInsets.symmetric( + vertical: 15.0, + horizontal: 8.0, + ), + child: Icon(Icons.more_vert), + ), + ), + ); + }, + menuChildren: [ Consumer( - builder: (context, ref, child) { - final history = ref.watch( - tabStateProvider( - selectedTabId, - ).select((value) => value?.historyState), + builder: (context, childRef, child) { + final pageExtensions = childRef.watch( + webExtensionsStateProvider( + WebExtensionActionType.page, + ).select((value) => value.values.toList()), ); - final isLoading = ref.watch( - selectedTabStateProvider.select( - (state) => state?.isLoading ?? false, - ), - ); - - return Row( + return Wrap( + alignment: WrapAlignment.center, children: [ - Expanded( - child: - (history?.canGoBack == true || isLoading) - ? IconButton( - onPressed: () async { - final controller = ref.read( - tabSessionProvider( - tabId: selectedTabId, - ).notifier, - ); - - if (isLoading) { - await controller.stopLoading(); - } else { - await controller.goBack(); - } - - trippleDotMenuController.close(); - }, - icon: const Icon(Icons.arrow_back), - ) - : IconButton( - onPressed: () async { - await ref - .read(tabRepositoryProvider.notifier) - .closeTab(selectedTabId); - trippleDotMenuController.close(); - }, - icon: const Icon(Icons.close), - ), - ), - const SizedBox(height: 48, child: VerticalDivider()), - Expanded( - child: IconButton( - onPressed: - (history?.canGoForward == true) - ? () async { - final controller = ref.read( - tabSessionProvider( - tabId: selectedTabId, - ).notifier, - ); - - await controller.goForward(); - trippleDotMenuController.close(); - } - : null, - icon: const Icon(Icons.arrow_forward), + ...pageExtensions.map( + (extension) => IconButton( + onPressed: () async { + //Use parents .ref because after onPressed this consumer gets disposed already + await addonService.invokeAddonAction( + extension.extensionId, + WebExtensionActionType.page, + ); + }, + icon: ExtensionBadgeIcon(extension), ), ), ], ); }, ), - ], - ), - ], + MenuItemButton( + onPressed: () async { + await AboutRoute().push(context); + }, + leadingIcon: const Icon(Icons.info), + child: const Text('About'), + ), + const Divider(), + MenuItemButton( + onPressed: () async { + await SettingsRoute().push(context); + }, + leadingIcon: const Icon(Icons.settings), + child: const Text('Settings'), + ), + Consumer( + builder: (context, ref, child) { + final browserExtensions = ref.watch( + webExtensionsStateProvider( + WebExtensionActionType.browser, + ).select((value) => value.values.toList()), + ); + + return SubmenuButton( + menuChildren: [ + ...browserExtensions.map( + (extension) => MenuItemButton( + onPressed: () async { + //Use parents .ref because after onPressed this consumer gets disposed already + await addonService.invokeAddonAction( + extension.extensionId, + WebExtensionActionType.browser, + ); + }, + leadingIcon: ExtensionBadgeIcon(extension), + child: Text(extension.title ?? ''), + ), + ), + MenuItemButton( + onPressed: () async { + await addonService.startAddonManagerActivity(); + }, + leadingIcon: const Icon(MdiIcons.puzzleEdit), + child: const Text('Addon Manager'), + ), + ], + leadingIcon: const Icon(MdiIcons.puzzle), + child: const Text('Addons'), + ); + }, + ), + MenuItemButton( + onPressed: () async { + await TorProxyRoute().push(context); + }, + leadingIcon: const Icon(TorIcons.onionAlt), + child: const Text('Tor'), + ), + const Divider(), + MenuItemButton( + onPressed: () async { + await BangCategoriesRoute().push(context); + }, + leadingIcon: const Icon(MdiIcons.exclamationThick), + child: const Text('Bangs'), + ), + MenuItemButton( + onPressed: () async { + await context.push(FeedListRoute().location); + }, + leadingIcon: const Icon(Icons.rss_feed), + child: const Text('Feeds'), + ), + const Divider(), + if (selectedTabId != null) + MenuItemButton( + onPressed: () async { + final tabState = ref.read( + tabStateProvider(selectedTabId), + ); + + if (tabState?.url case final Uri url) { + await ui_helper.launchUrlFeedback(context, url); + } + }, + leadingIcon: const Icon(Icons.open_in_browser), + child: const Text('Launch External'), + ), + if (selectedTabId != null) + MenuItemButton( + onPressed: () async { + final tabState = ref.read( + tabStateProvider(selectedTabId), + ); + + if (tabState?.url case final Uri url) { + await SharePlus.instance.share(ShareParams(uri: url)); + } + }, + leadingIcon: const Icon(Icons.share), + child: const Text('Share'), + ), + if (selectedTabId != null) const Divider(), + if (selectedTabId != null) + MenuItemButton( + onPressed: () { + ref.read(findInPageControllerProvider.notifier).show(); + }, + leadingIcon: const Icon(Icons.search), + child: const Text('Find in page'), + ), + if (selectedTabId != null) const Divider(), + if (selectedTabId != null) + MenuItemButton( + onPressed: () async { + final controller = ref.read( + tabSessionProvider(tabId: selectedTabId).notifier, + ); + + await controller.reload(); + trippleDotMenuController.close(); + }, + leadingIcon: const Icon(Icons.refresh), + child: const Text('Reload'), + ), + if (selectedTabId != null) const Divider(), + if (selectedTabId != null) + 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: + (history?.canGoBack == true || isLoading) + ? IconButton( + onPressed: () async { + final controller = ref.read( + tabSessionProvider( + tabId: selectedTabId, + ).notifier, + ); + + if (isLoading) { + await controller.stopLoading(); + } else { + await controller.goBack(); + } + + trippleDotMenuController.close(); + }, + icon: const Icon(Icons.arrow_back), + ) + : IconButton( + onPressed: () async { + await ref + .read( + tabRepositoryProvider.notifier, + ) + .closeTab(selectedTabId); + trippleDotMenuController.close(); + }, + icon: const Icon(Icons.close), + ), + ), + const SizedBox(height: 48, child: VerticalDivider()), + Expanded( + child: IconButton( + onPressed: + (history?.canGoForward == true) + ? () async { + final controller = ref.read( + tabSessionProvider( + tabId: selectedTabId, + ).notifier, + ); + + await controller.goForward(); + trippleDotMenuController.close(); + } + : null, + icon: const Icon(Icons.arrow_forward), + ), + ), + ], + ); + }, + ), + ], + ), + ], + ), ), ); }