From 2bfb3fdf8796385c80987e6350f2e320a085a678 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 5 Nov 2025 07:57:52 +0100 Subject: [PATCH] reorganize tab bar menus; readerview button visibility via setting; --- .../browser_modules/bottom_app_bar.dart | 156 +++++----- .../presentation/widgets/sheets/view_tab.dart | 197 ------------ .../presentation/widgets/tab_menu.dart | 292 ++++++++++++++++++ .../presentation/widgets/reader_button.dart | 32 +- .../screens/container_draft_suggestions.dart | 2 +- .../screens/general_settings.dart | 16 + .../user/data/models/general_settings.dart | 7 +- .../user/data/models/general_settings.g.dart | 16 + .../domain/repositories/general_settings.dart | 4 + .../repositories/general_settings.g.dart | 2 +- 10 files changed, 418 insertions(+), 306 deletions(-) create mode 100644 app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart 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 135db378..2955bf2e 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 @@ -24,12 +24,11 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:share_plus/share_plus.dart'; import 'package:weblibre/core/providers/defaults.dart'; import 'package:weblibre/core/routing/routes.dart'; 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/desktop_mode.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'; @@ -40,8 +39,8 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget 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/tab_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/controllers/readerable.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; @@ -62,6 +61,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { final tabMenuController = useMenuController(); final extensionMenuController = useMenuController(); + final hamburgerMenuController = useMenuController(); final trippleDotMenuController = useMenuController(); final selectedTabId = ref.watch(selectedTabProvider); @@ -138,7 +138,48 @@ class BrowserBottomAppBar extends HookConsumerWidget { : null, actions: [ if (selectedTabId != null && displayedSheet is! ViewTabsSheet) - ReaderButton(), + Consumer( + builder: (context, ref, child) { + final tabBarReaderView = ref.watch( + generalSettingsWithDefaultsProvider.select( + (value) => value.tabBarReaderView, + ), + ); + + final readerabilityStateActive = ref.watch( + selectedTabStateProvider.select( + (state) => + (state?.readerableState ?? ReaderableState.$default()) + .active, + ), + ); + + return Visibility( + visible: tabBarReaderView || readerabilityStateActive, + child: ReaderButton( + buttonBuilder: (isLoading, readerActive, icon) => InkWell( + onTap: isLoading + ? null + : () async { + await ref + .read( + readerableScreenControllerProvider + .notifier, + ) + .toggleReaderView(!readerActive); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 15.0, + horizontal: 8.0, + ), + child: icon, + ), + ), + ), + ); + }, + ), if (showExtensionShortcut) ExtensionShortcutMenu( controller: extensionMenuController, @@ -153,6 +194,27 @@ class BrowserBottomAppBar extends HookConsumerWidget { icon: const Icon(MdiIcons.puzzle), ), ), + if (selectedTabId != null) + TabMenu( + controller: trippleDotMenuController, + selectedTabId: selectedTabId, + child: InkWell( + onTap: () { + if (trippleDotMenuController.isOpen) { + trippleDotMenuController.close(); + } else { + trippleDotMenuController.open(); + } + }, + child: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 15.0, + ), + child: Icon(MdiIcons.dotsHorizontal), + ), + ), + ), TabCreationMenu( controller: tabMenuController, selectedTabId: selectedTabId, @@ -187,7 +249,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { ), ), MenuAnchor( - controller: trippleDotMenuController, + controller: hamburgerMenuController, builder: (context, controller, child) { return Padding( padding: const EdgeInsets.only(right: 4.0), @@ -204,7 +266,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { vertical: 15.0, horizontal: 8.0, ), - child: Icon(Icons.more_vert), + child: Icon(Icons.menu), ), ), ); @@ -377,80 +439,6 @@ class BrowserBottomAppBar extends HookConsumerWidget { 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: () { - final tabId = ref.read(selectedTabProvider); - if (tabId != null) { - ref - .read(findInPageControllerProvider(tabId).notifier) - .show(); - } - }, - leadingIcon: const Icon(Icons.search), - child: const Text('Find in page'), - ), - if (selectedTabId != null) - Consumer( - builder: (context, childRef, child) { - final enabled = childRef.watch( - desktopModeProvider(selectedTabId), - ); - - return MenuItemButton( - onPressed: () { - ref - .read(desktopModeProvider(selectedTabId).notifier) - .toggle(); - }, - leadingIcon: const Icon(MdiIcons.monitor), - trailingIcon: Checkbox( - value: enabled, - onChanged: (value) { - if (value != null) { - ref - .read( - desktopModeProvider(selectedTabId).notifier, - ) - .enabled(value); - trippleDotMenuController.close(); - } - }, - ), - child: const Text('Desktop Mode'), - ); - }, - ), - if (selectedTabId != null) const Divider(), if (selectedTabId != null) MenuItemButton( onPressed: () async { @@ -459,7 +447,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { ); await controller.reload(); - trippleDotMenuController.close(); + hamburgerMenuController.close(); }, leadingIcon: const Icon(Icons.refresh), child: const Text('Reload'), @@ -513,7 +501,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { await controller.goBack(); } - trippleDotMenuController.close(); + hamburgerMenuController.close(); }, icon: const Icon(Icons.arrow_back), ) @@ -523,7 +511,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { .read(tabRepositoryProvider.notifier) .closeTab(selectedTabId); - trippleDotMenuController.close(); + hamburgerMenuController.close(); if (context.mounted) { ui_helper.showTabUndoClose( @@ -551,7 +539,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { ); await controller.goForward(); - trippleDotMenuController.close(); + hamburgerMenuController.close(); } : null, icon: const Icon(Icons.arrow_forward), diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart index f7200859..e7d72324 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart @@ -17,33 +17,18 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import 'dart:ui' as ui; import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.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:share_plus/share_plus.dart'; -import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; import 'package:weblibre/features/bangs/presentation/widgets/site_search.dart'; -import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; -import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart'; -import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; -import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/certificate_tile.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; -import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; -import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; -import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/presentation/hooks/listenable_callback.dart'; -import 'package:weblibre/presentation/widgets/share_tile.dart'; -import 'package:weblibre/presentation/widgets/website_feed_tile.dart'; import 'package:weblibre/presentation/widgets/website_title_tile.dart'; -import 'package:weblibre/utils/ui_helper.dart' as ui_helper; class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics { const ClampingScrollPhysicsWithoutImplicit({super.parent}); @@ -220,188 +205,6 @@ class ViewTabSheetWidget extends HookConsumerWidget { ), ), const Divider(), - ListTile( - leading: const Icon(MdiIcons.contentCopy), - title: const Text('Copy address'), - onTap: () async { - await Clipboard.setData( - ClipboardData(text: initialTabState.url.toString()), - ); - - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); - }, - ), - ListTile( - onTap: () async { - await ui_helper.launchUrlFeedback( - context, - initialTabState.url, - ); - }, - leading: const Icon(Icons.open_in_browser), - title: const Text('Launch External'), - ), - ListTile( - leading: const Icon(MdiIcons.tabPlus), - title: const Text('Clone tab'), - onTap: () async { - final tabId = await ref - .read(tabRepositoryProvider.notifier) - .addTab( - url: initialTabState.url, - private: false, - selectTab: false, - ); - - if (context.mounted) { - //save reference before pop `ref` gets disposed - final repo = ref.read(tabRepositoryProvider.notifier); - - ui_helper.showTabSwitchMessage( - context, - onSwitch: () async { - await repo.selectTab(tabId); - }, - ); - - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); - } - }, - ), - ListTile( - leading: const Icon(MdiIcons.tabUnselected), - title: const Text('Clone as private tab'), - onTap: () async { - final tabId = await ref - .read(tabRepositoryProvider.notifier) - .addTab( - url: initialTabState.url, - private: true, - selectTab: false, - ); - - if (context.mounted) { - //save reference before pop `ref` gets disposed - final repo = ref.read(tabRepositoryProvider.notifier); - - ui_helper.showTabSwitchMessage( - context, - onSwitch: () async { - await repo.selectTab(tabId); - }, - ); - - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); - } - }, - ), - ListTile( - leading: const Icon(MdiIcons.folderArrowUpDownOutline), - title: const Text('Assign container'), - onTap: () async { - final targetContainerId = await ContainerSelectionRoute() - .push(context); - - if (targetContainerId != null) { - final containerData = await ref - .read(containerRepositoryProvider.notifier) - .getContainerData(targetContainerId); - - if (containerData != null) { - await ref - .read(tabDataRepositoryProvider.notifier) - .assignContainer(initialTabState.id, containerData); - } - } - - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); - }, - ), - Consumer( - child: ListTile( - leading: const Icon(MdiIcons.folderCancelOutline), - title: const Text('Unassign container'), - onTap: () async { - await ref - .read(tabDataRepositoryProvider.notifier) - .unassignContainer(initialTabState.id); - - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); - }, - ), - builder: (context, ref, child) { - final containerId = ref.watch( - watchContainerTabIdProvider( - initialTabState.id, - ).select((value) => value.value), - ); - - return Visibility( - visible: containerId != null, - child: child!, - ); - }, - ), - ShareTile( - onTap: () async { - await SharePlus.instance.share( - ShareParams(uri: initialTabState.url), - ); - - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); - }, - onTapQr: () async { - await showQrCode(context, initialTabState.url.toString()); - }, - ), - ListTile( - leading: const Icon(Icons.mobile_screen_share), - title: const Text('Share screenshot'), - onTap: () async { - final screenshot = await ref - .read(selectedTabSessionProvider) - .requestScreenshot(); - - if (screenshot != null) { - ui.decodeImageFromList(screenshot, (result) async { - final png = await result.toByteData( - format: ui.ImageByteFormat.png, - ); - - if (png != null) { - final file = XFile.fromData( - png.buffer.asUint8List(), - mimeType: 'image/png', - ); - - await SharePlus.instance.share( - ShareParams( - files: [file], - subject: initialTabState.title, - ), - ); - } - }); - } - - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); - }, - ), - WebsiteFeedTile(initialTabState), ], ); }, 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 new file mode 100644 index 00000000..19fcf360 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart @@ -0,0 +1,292 @@ +/* + * 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 . + */ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:share_plus/share_plus.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/dialogs/qr_code.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'; +import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; +import 'package:weblibre/presentation/widgets/website_feed_menu_button.dart'; +import 'package:weblibre/utils/ui_helper.dart' as ui_helper; + +class TabMenu extends HookConsumerWidget { + final Widget child; + final MenuController controller; + final String selectedTabId; + + const TabMenu({ + super.key, + required this.child, + required this.controller, + required this.selectedTabId, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return MenuAnchor( + controller: controller, + builder: (context, controller, child) { + return child!; + }, + menuChildren: [ + MenuItemButton( + onPressed: () { + ref.read(bottomSheetControllerProvider.notifier).requestDismiss(); + + ref + .read(findInPageControllerProvider(selectedTabId).notifier) + .show(); + }, + leadingIcon: const Icon(Icons.search), + child: const Text('Find in page'), + ), + ReaderButton( + buttonBuilder: (isLoading, readerActive, icon) => MenuItemButton( + onPressed: isLoading + ? null + : () async { + await ref + .read(readerableScreenControllerProvider.notifier) + .toggleReaderView(!readerActive); + }, + leadingIcon: icon, + trailingIcon: Checkbox( + value: readerActive, + onChanged: (value) async { + if (value != null && !isLoading) { + await ref + .read(readerableScreenControllerProvider.notifier) + .toggleReaderView(!readerActive); + controller.close(); + } + }, + ), + child: const Text('Reader Mode'), + ), + ), + Consumer( + builder: (context, childRef, child) { + final enabled = childRef.watch(desktopModeProvider(selectedTabId)); + + return MenuItemButton( + onPressed: () { + ref.read(desktopModeProvider(selectedTabId).notifier).toggle(); + }, + leadingIcon: const Icon(MdiIcons.monitor), + trailingIcon: Checkbox( + value: enabled, + onChanged: (value) { + if (value != null) { + ref + .read(desktopModeProvider(selectedTabId).notifier) + .enabled(value); + controller.close(); + } + }, + ), + child: const Text('Desktop Mode'), + ); + }, + ), + const Divider(), + MenuItemButton( + leadingIcon: const Icon(MdiIcons.contentCopy), + child: const Text('Copy address'), + onPressed: () async { + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + await Clipboard.setData( + ClipboardData(text: tabState.url.toString()), + ); + }, + ), + MenuItemButton( + leadingIcon: const Icon(Icons.open_in_browser), + child: const Text('Launch External'), + onPressed: () async { + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + await ui_helper.launchUrlFeedback(context, tabState.url); + }, + ), + MenuItemButton( + leadingIcon: const Icon(MdiIcons.tabPlus), + child: const Text('Clone tab'), + onPressed: () async { + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + final tabId = await ref + .read(tabRepositoryProvider.notifier) + .addTab(url: tabState.url, private: false, selectTab: false); + + if (context.mounted) { + //save reference before pop `ref` gets disposed + final repo = ref.read(tabRepositoryProvider.notifier); + + ui_helper.showTabSwitchMessage( + context, + onSwitch: () async { + await repo.selectTab(tabId); + }, + ); + } + }, + ), + MenuItemButton( + leadingIcon: const Icon(MdiIcons.tabUnselected), + child: const Text('Clone as private tab'), + onPressed: () async { + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + final tabId = await ref + .read(tabRepositoryProvider.notifier) + .addTab(url: tabState.url, private: true, selectTab: false); + + if (context.mounted) { + //save reference before pop `ref` gets disposed + final repo = ref.read(tabRepositoryProvider.notifier); + + ui_helper.showTabSwitchMessage( + context, + onSwitch: () async { + await repo.selectTab(tabId); + }, + ); + } + }, + ), + MenuItemButton( + leadingIcon: const Icon(MdiIcons.folderArrowUpDownOutline), + child: const Text('Assign container'), + onPressed: () async { + final targetContainerId = await ContainerSelectionRoute() + .push(context); + + if (targetContainerId != null) { + final containerData = await ref + .read(containerRepositoryProvider.notifier) + .getContainerData(targetContainerId); + + if (containerData != null) { + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + await ref + .read(tabDataRepositoryProvider.notifier) + .assignContainer(tabState.id, containerData); + } + } + }, + ), + Consumer( + child: MenuItemButton( + leadingIcon: const Icon(MdiIcons.folderCancelOutline), + child: const Text('Unassign container'), + onPressed: () async { + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + await ref + .read(tabDataRepositoryProvider.notifier) + .unassignContainer(tabState.id); + }, + ), + builder: (context, ref, child) { + final containerId = ref.watch( + watchContainerTabIdProvider( + selectedTabId, + ).select((value) => value.value), + ); + + return Visibility(visible: containerId != null, child: child!); + }, + ), + MenuItemButton( + leadingIcon: const Icon(Icons.share), + onPressed: () async { + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + await SharePlus.instance.share(ShareParams(uri: tabState.url)); + }, + trailingIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const VerticalDivider(indent: 4, endIndent: 4), + IconButton( + icon: const Icon(Icons.qr_code), + onPressed: () async { + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + await showQrCode(context, tabState.url.toString()); + controller.close(); + }, + ), + ], + ), + child: const Text('Share link'), + ), + MenuItemButton( + leadingIcon: const Icon(Icons.mobile_screen_share), + child: const Text('Share screenshot'), + onPressed: () async { + final screenshot = await ref + .read(selectedTabSessionProvider) + .requestScreenshot(); + + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + if (screenshot != null) { + ui.decodeImageFromList(screenshot, (result) async { + final png = await result.toByteData( + format: ui.ImageByteFormat.png, + ); + + if (png != null) { + final file = XFile.fromData( + png.buffer.asUint8List(), + mimeType: 'image/png', + ); + + await SharePlus.instance.share( + ShareParams(files: [file], subject: tabState.title), + ); + } + }); + } + }, + ), + WebsiteFeedMenuButton(selectedTabId), + ], + child: child, + ); + } +} diff --git a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart index 43668dd3..6b1205c8 100644 --- a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart +++ b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart @@ -27,6 +27,11 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart import 'package:weblibre/presentation/widgets/animate_gradient_shader.dart'; class ReaderButton extends HookConsumerWidget { + final Widget Function(bool isLoading, bool readerActive, Widget icon) + buttonBuilder; + + const ReaderButton({super.key, required this.buttonBuilder}); + @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; @@ -65,21 +70,10 @@ class ReaderButton extends HookConsumerWidget { (enforceReadability && enableReadability), child: readerChanging.when( skipLoadingOnReload: true, - data: (_) => InkWell( - onTap: readerChanging.isLoading - ? null - : () async { - await ref - .read(readerableScreenControllerProvider.notifier) - .toggleReaderView(!readerabilityState.active); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 15.0, - horizontal: 8.0, - ), - child: icon, - ), + data: (_) => buttonBuilder( + readerChanging.isLoading, + readerabilityState.active, + icon, ), error: (error, stackTrace) => SizedBox.shrink(), loading: () => AnimateGradientShader( @@ -91,13 +85,7 @@ class ReaderButton extends HookConsumerWidget { colorScheme.secondary, colorScheme.secondaryContainer, ], - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 15.0, - horizontal: 8.0, - ), - child: icon, - ), + child: buttonBuilder(true, readerabilityState.active, icon), ), ), ); diff --git a/app/lib/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart b/app/lib/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart index 2eb7b1e5..3525fc8b 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart @@ -10,8 +10,8 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/core/uuid.dart'; -import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/view_tabs.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/view_tabs.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart'; diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart index 81703f35..e3e5272f 100644 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -346,6 +346,22 @@ class GeneralSettingsScreen extends HookConsumerWidget { } : null, ), + SwitchListTile.adaptive( + title: const Text('Reader Mode in Tab Bar'), + subtitle: const Text( + 'Show reader mode button in the tab bar instead of only in the tab menu', + ), + secondary: const Icon(MdiIcons.bookHeart), + value: generalSettings.tabBarReaderView, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.tabBarReaderView(value), + ); + }, + ), SwitchListTile.adaptive( title: const Text('Create Child Tabs'), subtitle: const Text( diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index cf1e7fe0..01186d5a 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -71,6 +71,7 @@ class GeneralSettings with FastEquatable { final TabBarSwipeAction tabBarSwipeAction; final Duration historyAutoCleanInterval; final bool tabViewBottomSheet; + final bool tabBarReaderView; GeneralSettings({ required this.themeMode, @@ -88,6 +89,7 @@ class GeneralSettings with FastEquatable { required this.tabBarSwipeAction, required this.historyAutoCleanInterval, required this.tabViewBottomSheet, + required this.tabBarReaderView, }); GeneralSettings.withDefaults({ @@ -106,6 +108,7 @@ class GeneralSettings with FastEquatable { TabBarSwipeAction? tabBarSwipeAction, Duration? historyAutoCleanInterval, bool? tabViewBottomSheet, + bool? tabBarReaderView, }) : themeMode = themeMode ?? ThemeMode.dark, enableReadability = enableReadability ?? true, enforceReadability = enforceReadability ?? false, @@ -122,7 +125,8 @@ class GeneralSettings with FastEquatable { tabBarSwipeAction ?? TabBarSwipeAction.switchLastOpened, historyAutoCleanInterval = historyAutoCleanInterval ?? const Duration(days: 90), - tabViewBottomSheet = tabViewBottomSheet ?? false; + tabViewBottomSheet = tabViewBottomSheet ?? false, + tabBarReaderView = tabBarReaderView ?? false; factory GeneralSettings.fromJson(Map json) => _$GeneralSettingsFromJson(json); @@ -146,5 +150,6 @@ class GeneralSettings with FastEquatable { tabBarSwipeAction, historyAutoCleanInterval, tabViewBottomSheet, + tabBarReaderView, ]; } 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 cd5559a7..7f7d5faf 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -43,6 +43,8 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings tabViewBottomSheet(bool tabViewBottomSheet); + GeneralSettings tabBarReaderView(bool tabBarReaderView); + /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. /// @@ -66,6 +68,7 @@ abstract class _$GeneralSettingsCWProxy { TabBarSwipeAction tabBarSwipeAction, Duration historyAutoCleanInterval, bool tabViewBottomSheet, + bool tabBarReaderView, }); } @@ -138,6 +141,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings tabViewBottomSheet(bool tabViewBottomSheet) => call(tabViewBottomSheet: tabViewBottomSheet); + @override + GeneralSettings tabBarReaderView(bool tabBarReaderView) => + call(tabBarReaderView: tabBarReaderView); + @override /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. @@ -162,6 +169,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? tabBarSwipeAction = const $CopyWithPlaceholder(), Object? historyAutoCleanInterval = const $CopyWithPlaceholder(), Object? tabViewBottomSheet = const $CopyWithPlaceholder(), + Object? tabBarReaderView = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null @@ -250,6 +258,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.tabViewBottomSheet // ignore: cast_nullable_to_non_nullable : tabViewBottomSheet as bool, + tabBarReaderView: + tabBarReaderView == const $CopyWithPlaceholder() || + tabBarReaderView == null + ? _value.tabBarReaderView + // ignore: cast_nullable_to_non_nullable + : tabBarReaderView as bool, ); } } @@ -303,6 +317,7 @@ GeneralSettings _$GeneralSettingsFromJson(Map json) => microseconds: (json['historyAutoCleanInterval'] as num).toInt(), ), tabViewBottomSheet: json['tabViewBottomSheet'] as bool?, + tabBarReaderView: json['tabBarReaderView'] as bool?, ); Map _$GeneralSettingsToJson( @@ -330,6 +345,7 @@ Map _$GeneralSettingsToJson( 'tabBarSwipeAction': _$TabBarSwipeActionEnumMap[instance.tabBarSwipeAction]!, 'historyAutoCleanInterval': instance.historyAutoCleanInterval.inMicroseconds, 'tabViewBottomSheet': instance.tabViewBottomSheet, + 'tabBarReaderView': instance.tabBarReaderView, }; const _$ThemeModeEnumMap = { diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index eb9ba6b6..b06966f6 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -104,6 +104,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.bool, db.typeMapping, ), + 'tabBarReaderView': settings['tabBarReaderView']?.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 3915cf39..dd111f19 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider } String _$generalSettingsRepositoryHash() => - r'7ab8eb72c43ed9486bf9fa8b13a6cce83e2b3d66'; + r'e929fd7a8bf315b08ea7f5a4df2bd5bf066bc84f'; abstract class _$GeneralSettingsRepository extends $StreamNotifier {