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 {