reorganize tab bar menus; readerview button visibility via setting;
This commit is contained in:
+72
-84
@@ -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),
|
||||
|
||||
-197
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+10
-22
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+1
-1
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user