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 80108d96..d399c6f1 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 @@ -18,10 +18,12 @@ import 'package:weblibre/features/geckoview/features/browser/domain/entities/she import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/edit_url_dialog.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/tab_creation_menu.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart'; import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/icons/tor_icons.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; @@ -36,6 +38,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { final addonService = ref.watch(addonServiceProvider); final tabMenuController = useMenuController(); + final extensionMenuController = useMenuController(); final trippleDotMenuController = useMenuController(); final selectedTabId = ref.watch(selectedTabProvider); @@ -43,6 +46,12 @@ class BrowserBottomAppBar extends HookConsumerWidget { selectedTabStateProvider.select((state) => state?.isPrivate ?? false), ); + final showExtensionShortcut = ref.watch( + generalSettingsRepositoryProvider.select( + (value) => value.showExtensionShortcut, + ), + ); + final dragStartPosition = useRef(Offset.zero); return BottomAppBar( @@ -97,6 +106,20 @@ class BrowserBottomAppBar extends HookConsumerWidget { actions: [ if (selectedTabId != null && displayedSheet is! ViewTabsSheet) ReaderButton(), + if (showExtensionShortcut) + ExtensionShortcutMenu( + controller: extensionMenuController, + child: IconButton( + onPressed: () { + if (extensionMenuController.isOpen) { + extensionMenuController.close(); + } else { + extensionMenuController.open(); + } + }, + icon: const Icon(MdiIcons.puzzle), + ), + ), TabCreationMenu( controller: tabMenuController, selectedTabId: selectedTabId, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart new file mode 100644 index 00000000..37559b82 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/domain/providers.dart'; +import 'package:weblibre/features/geckoview/domain/providers/web_extensions_state.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart'; + +class ExtensionShortcutMenu extends HookConsumerWidget { + final Widget child; + final MenuController controller; + + const ExtensionShortcutMenu({ + super.key, + required this.child, + required this.controller, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final browserExtensions = ref.watch( + webExtensionsStateProvider( + WebExtensionActionType.browser, + ).select((value) => value.values.toList()), + ); + + return MenuAnchor( + controller: controller, + builder: (context, controller, child) { + return child!; + }, + menuChildren: [ + ...browserExtensions.map( + (extension) => MenuItemButton( + onPressed: () async { + //Use parents .ref because after onPressed this consumer gets disposed already + await ref + .read(addonServiceProvider) + .invokeAddonAction( + extension.extensionId, + WebExtensionActionType.browser, + ); + }, + leadingIcon: ExtensionBadgeIcon(extension), + child: Text(extension.title ?? ''), + ), + ), + ], + child: Visibility(visible: browserExtensions.isNotEmpty, child: child), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart index dc76f6aa..bf3830cd 100644 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -210,6 +210,22 @@ class GeneralSettingsScreen extends HookConsumerWidget { ); }, ), + SwitchListTile.adaptive( + title: const Text('Show Extension Shortcut'), + subtitle: const Text( + 'Display an extension menu directly on the tab bar', + ), + secondary: const Icon(MdiIcons.puzzleHeart), + value: generalSettings.showExtensionShortcut, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .showExtensionShortcut(value), + ); + }, + ), Consumer( builder: (context, ref, child) { final size = ref.watch( diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index d2358976..7339709a 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -33,6 +33,7 @@ class GeneralSettings with FastEquatable { final String defaultSearchProvider; final SearchSuggestionProviders defaultSearchSuggestionsProvider; final bool createChildTabsOption; + final bool showExtensionShortcut; final bool proxyPrivateTabsTor; @@ -45,6 +46,7 @@ class GeneralSettings with FastEquatable { required this.defaultSearchSuggestionsProvider, required this.createChildTabsOption, required this.proxyPrivateTabsTor, + required this.showExtensionShortcut, }); GeneralSettings.withDefaults({ @@ -56,6 +58,7 @@ class GeneralSettings with FastEquatable { SearchSuggestionProviders? defaultSearchSuggestionsProvider, bool? createChildTabsOption, bool? proxyPrivateTabsTor, + bool? showExtensionShortcut, }) : themeMode = themeMode ?? ThemeMode.dark, enableReadability = enableReadability ?? true, enforceReadability = enforceReadability ?? false, @@ -63,7 +66,8 @@ class GeneralSettings with FastEquatable { defaultSearchSuggestionsProvider = defaultSearchSuggestionsProvider ?? _fallbackAutocompleteProvider, createChildTabsOption = createChildTabsOption ?? false, - proxyPrivateTabsTor = proxyPrivateTabsTor ?? false; + proxyPrivateTabsTor = proxyPrivateTabsTor ?? false, + showExtensionShortcut = showExtensionShortcut ?? false; factory GeneralSettings.fromJson(Map json) => _$GeneralSettingsFromJson(json); @@ -79,6 +83,7 @@ class GeneralSettings with FastEquatable { defaultSearchProvider, defaultSearchSuggestionsProvider, createChildTabsOption, + showExtensionShortcut, proxyPrivateTabsTor, ]; } diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart index 32be39a8..d05e1ecb 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -27,6 +27,8 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings proxyPrivateTabsTor(bool proxyPrivateTabsTor); + GeneralSettings showExtensionShortcut(bool showExtensionShortcut); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -42,6 +44,7 @@ abstract class _$GeneralSettingsCWProxy { SearchSuggestionProviders defaultSearchSuggestionsProvider, bool createChildTabsOption, bool proxyPrivateTabsTor, + bool showExtensionShortcut, }); } @@ -84,6 +87,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings proxyPrivateTabsTor(bool proxyPrivateTabsTor) => this(proxyPrivateTabsTor: proxyPrivateTabsTor); + @override + GeneralSettings showExtensionShortcut(bool showExtensionShortcut) => + this(showExtensionShortcut: showExtensionShortcut); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// @@ -100,6 +107,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? defaultSearchSuggestionsProvider = const $CopyWithPlaceholder(), Object? createChildTabsOption = const $CopyWithPlaceholder(), Object? proxyPrivateTabsTor = const $CopyWithPlaceholder(), + Object? showExtensionShortcut = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() @@ -138,6 +146,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.proxyPrivateTabsTor // ignore: cast_nullable_to_non_nullable : proxyPrivateTabsTor as bool, + showExtensionShortcut: + showExtensionShortcut == const $CopyWithPlaceholder() + ? _value.showExtensionShortcut + // ignore: cast_nullable_to_non_nullable + : showExtensionShortcut as bool, ); } } @@ -168,6 +181,7 @@ GeneralSettings _$GeneralSettingsFromJson(Map json) => ), createChildTabsOption: json['createChildTabsOption'] as bool?, proxyPrivateTabsTor: json['proxyPrivateTabsTor'] as bool?, + showExtensionShortcut: json['showExtensionShortcut'] as bool?, ); Map _$GeneralSettingsToJson(GeneralSettings instance) => @@ -183,6 +197,7 @@ Map _$GeneralSettingsToJson(GeneralSettings instance) => _$SearchSuggestionProvidersEnumMap[instance .defaultSearchSuggestionsProvider]!, 'createChildTabsOption': instance.createChildTabsOption, + 'showExtensionShortcut': instance.showExtensionShortcut, 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, }; diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index 76988344..06c7e6d9 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -56,6 +56,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.bool, db.typeMapping, ), + 'showExtensionShortcut': settings['showExtensionShortcut']?.readAs( + DriftSqlType.bool, + db.typeMapping, + ), }); } diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart index 63b48338..c1d85d8c 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -7,7 +7,7 @@ part of 'general_settings.dart'; // ************************************************************************** String _$generalSettingsRepositoryHash() => - r'5335acea5fb175b665b80b2241ce463ae8ad9f89'; + r'eb5f27e0bd8f5def833e1c703cb42181acc2440f'; /// See also [GeneralSettingsRepository]. @ProviderFor(GeneralSettingsRepository)