reorganize tab bar menus; readerview button visibility via setting;

This commit is contained in:
Fabian Freund
2025-11-05 07:57:52 +01:00
parent 55305c3dd1
commit 2bfb3fdf87
10 changed files with 418 additions and 306 deletions
@@ -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),
@@ -17,33 +17,18 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<String?>(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),
],
);
},
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String?>(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,
);
}
}
@@ -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),
),
),
);
@@ -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';
@@ -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(
@@ -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<String, dynamic> json) =>
_$GeneralSettingsFromJson(json);
@@ -146,5 +150,6 @@ class GeneralSettings with FastEquatable {
tabBarSwipeAction,
historyAutoCleanInterval,
tabViewBottomSheet,
tabBarReaderView,
];
}
@@ -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<String, dynamic> json) =>
microseconds: (json['historyAutoCleanInterval'] as num).toInt(),
),
tabViewBottomSheet: json['tabViewBottomSheet'] as bool?,
tabBarReaderView: json['tabBarReaderView'] as bool?,
);
Map<String, dynamic> _$GeneralSettingsToJson(
@@ -330,6 +345,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
'tabBarSwipeAction': _$TabBarSwipeActionEnumMap[instance.tabBarSwipeAction]!,
'historyAutoCleanInterval': instance.historyAutoCleanInterval.inMicroseconds,
'tabViewBottomSheet': instance.tabViewBottomSheet,
'tabBarReaderView': instance.tabBarReaderView,
};
const _$ThemeModeEnumMap = {
@@ -104,6 +104,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.bool,
db.typeMapping,
),
'tabBarReaderView': settings['tabBarReaderView']?.readAs(
DriftSqlType.bool,
db.typeMapping,
),
});
}
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
}
String _$generalSettingsRepositoryHash() =>
r'7ab8eb72c43ed9486bf9fa8b13a6cce83e2b3d66';
r'e929fd7a8bf315b08ea7f5a4df2bd5bf066bc84f';
abstract class _$GeneralSettingsRepository
extends $StreamNotifier<GeneralSettings> {