merge navigation and tab menu into bottom sheet

This commit is contained in:
Fabian Freund
2026-02-28 18:37:35 +01:00
parent 9be77d00e5
commit b7edd95093
19 changed files with 2095 additions and 536 deletions
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2024-2026 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:convert';
import 'package:riverpod/experimental/persist.dart';
import 'package:riverpod_annotation/experimental/persist.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/user/data/providers.dart';
part 'extensions_expanded.g.dart';
@Riverpod(keepAlive: true)
class ExtensionsExpanded extends _$ExtensionsExpanded {
void toggle() {
state = !state;
}
@override
bool build() {
persist(
ref.watch(riverpodDatabaseStorageProvider),
key: 'ExtensionsExpanded',
encode: (state) => jsonEncode([state]),
decode: (encoded) => (jsonDecode(encoded) as List<dynamic>).first as bool,
);
return stateOrNull ?? false;
}
}
@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'extensions_expanded.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ExtensionsExpanded)
final extensionsExpandedProvider = ExtensionsExpandedProvider._();
final class ExtensionsExpandedProvider
extends $NotifierProvider<ExtensionsExpanded, bool> {
ExtensionsExpandedProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'extensionsExpandedProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$extensionsExpandedHash();
@$internal
@override
ExtensionsExpanded create() => ExtensionsExpanded();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$extensionsExpandedHash() =>
r'c07d67f3d00a8c374b598664f7f54c1bcc6de5b7';
abstract class _$ExtensionsExpanded extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -47,7 +47,6 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/draggable_fab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/navigation_drawer.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart';
@@ -292,11 +291,6 @@ class BrowserScreen extends HookConsumerWidget {
),
);
final drawerGestureEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.drawerGestureEnabled,
),
);
ref.listen(overlayControllerProvider, (previous, next) {
if (next != null) {
@@ -567,8 +561,6 @@ class BrowserScreen extends HookConsumerWidget {
child: Scaffold(
// Minimal scaffold - only for Material overlay support (SnackBars)
resizeToAvoidBottomInset: false,
endDrawer: const BrowserNavigationDrawer(),
endDrawerEnableOpenDragGesture: drawerGestureEnabled,
body: Stack(
children: [
// Layer 0: Browser content
@@ -879,6 +871,13 @@ class _Browser extends HookConsumerWidget {
return false;
}
// Dismiss modal routes (e.g. showModalBottomSheet)
final rootNavigator = Navigator.of(context, rootNavigator: true);
if (rootNavigator.canPop()) {
rootNavigator.pop();
return true;
}
if (ref.read(bottomSheetControllerProvider) != null) {
ref
.read(bottomSheetControllerProvider.notifier)
@@ -38,6 +38,7 @@ import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart';
@@ -55,7 +56,6 @@ import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/menu_controller.dart';
import 'package:weblibre/presentation/icons/weblibre_icons.dart';
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
@@ -80,7 +80,6 @@ class BrowserTopAppBar extends HookConsumerWidget {
showQuickTabSwitcherBar: false,
showMainToolbarNavigationButton: !showContextualToolbar,
showMainToolbarTabsCount: !showContextualToolbar,
showMainToolbarTabActionButton: !showContextualToolbar,
);
}
@@ -117,7 +116,6 @@ class BrowserBottomAppBar extends HookConsumerWidget {
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
showMainToolbarNavigationButton: !showContextualToolbar,
showMainToolbarTabsCount: !showContextualToolbar,
showMainToolbarTabActionButton: !showContextualToolbar,
);
}
@@ -147,7 +145,6 @@ class BrowserTabBar extends HookConsumerWidget {
final bool showMainToolbarTabsCount;
final bool showMainToolbarNavigationButton;
final bool showMainToolbarTabActionButton;
const BrowserTabBar({
super.key,
@@ -157,7 +154,6 @@ class BrowserTabBar extends HookConsumerWidget {
required this.showQuickTabSwitcherBar,
required this.showMainToolbarTabsCount,
required this.showMainToolbarNavigationButton,
required this.showMainToolbarTabActionButton,
});
static const contextualToolabarHeight = 54.0;
@@ -191,7 +187,6 @@ class BrowserTabBar extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final extensionMenuController = useMenuController();
final trippleDotMenuController = useMenuController();
final selectedTabId = ref.watch(selectedTabProvider);
final settings = ref.watch(generalSettingsWithDefaultsProvider);
@@ -360,23 +355,6 @@ class BrowserTabBar extends HookConsumerWidget {
displayedSheet: displayedSheet,
showLongPressMenu: true,
),
if (selectedTabId != null && showMainToolbarTabActionButton)
TabMenu(
controller: trippleDotMenuController,
selectedTabId: selectedTabId,
builder: (context, controller, child) {
return ToolbarButton(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(WebLibreIcons.tabOptions),
);
},
),
],
),
),
@@ -444,24 +422,6 @@ class ContextualToolbar extends HookConsumerWidget {
displayedSheet: displayedSheet,
showLongPressMenu: false,
),
if (selectedTabId != null)
TabMenu(
controller: useMenuController(),
selectedTabId: selectedTabId!,
enableNavigationButtons: false,
builder: (context, controller, child) {
return ToolbarButton(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(WebLibreIcons.tabOptions),
);
},
),
],
);
}
@@ -679,8 +639,8 @@ class NavigationMenuButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ToolbarButton(
onTap: () {
Scaffold.of(context).openEndDrawer();
onTap: () async {
await showBrowserMenuSheet(context);
},
child: const Icon(Icons.menu),
);
@@ -1,422 +0,0 @@
/*
* Copyright (c) 2024-2026 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:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
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:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/domain/providers/web_extensions_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
import 'package:weblibre/features/user/domain/presentation/dialogs/quit_browser_dialog.dart';
import 'package:weblibre/features/user/domain/providers.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/icons/tor_icons.dart';
import 'package:weblibre/utils/exit_app.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
/// Navigation drawer for the browser screen.
/// Contains all navigation destinations and settings.
class BrowserNavigationDrawer extends HookConsumerWidget {
const BrowserNavigationDrawer({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final showContainerUi = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.showContainerUi),
);
return NavigationDrawer(
backgroundColor: colorScheme.surface,
header: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [_ProfileHeader(), _SyncTile(), const Divider()],
),
children: [
// Section 1: Tools & Configuration
_ExtensionsSection(),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Settings'),
onTap: () async {
Navigator.of(context).pop();
await SettingsRoute().push(context);
},
),
Consumer(
builder: (context, ref, child) {
final torConnected = ref.watch(
torProxyServiceProvider.select(
(value) => value.value?.isRunning == true,
),
);
return ListTile(
leading: Badge(
isLabelVisible: torConnected,
backgroundColor: AppColors.of(context).torActiveGreen,
child: const Icon(TorIcons.onionAlt),
),
title: const Text('Tor™ Proxy'),
onTap: () async {
Navigator.of(context).pop();
await const TorProxyRoute().push(context);
},
);
},
),
const Divider(),
// Section 2: Data & Content
ListTile(
leading: const Icon(Icons.history),
title: const Text('History'),
onTap: () async {
Navigator.of(context).pop();
await const HistoryRoute().push(context);
},
),
ListTile(
leading: const Icon(MdiIcons.fileDownload),
title: const Text('Downloads'),
onTap: () async {
Navigator.of(context).pop();
await const HistoryDownloadsRoute().push(context);
},
),
ListTile(
leading: const Icon(MdiIcons.bookmarkMultiple),
title: const Text('Bookmarks'),
onTap: () async {
Navigator.of(context).pop();
await BookmarkListRoute(
entryGuid: BookmarkRoot.root.id,
).push(context);
},
),
ListTile(
leading: const Icon(MdiIcons.exclamationThick),
title: const Text('Bangs'),
onTap: () async {
Navigator.of(context).pop();
await const BangMenuRoute().push(context);
},
),
if (showContainerUi)
ListTile(
leading: const Icon(MdiIcons.folder),
title: const Text('Containers'),
onTap: () async {
Navigator.of(context).pop();
await const ContainerListRoute().push(context);
},
),
ListTile(
leading: const Icon(Icons.rss_feed),
title: const Text('Feeds'),
onTap: () async {
Navigator.of(context).pop();
await context.push(FeedListRoute().location);
},
),
const Divider(),
// Section 3: App
ListTile(
leading: const Icon(Icons.info),
title: const Text('About'),
onTap: () async {
Navigator.of(context).pop();
await AboutRoute().push(context);
},
),
// Quit Browser - styled as destructive action
ListTile(
leading: Icon(MdiIcons.power, color: colorScheme.error),
title: Text(
'Quit Browser',
style: TextStyle(color: colorScheme.error),
),
onTap: () async {
final result = await showQuitBrowserDialog(context);
if (result == true && context.mounted) {
await exitApp(ProviderScope.containerOf(context));
}
},
),
],
);
}
}
/// Profile header widget showing current profile information.
class _ProfileHeader extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final profile = ref.watch(selectedProfileProvider);
return InkWell(
onTap: () async {
Navigator.of(context).pop(); // Close drawer
await const SelectProfileRoute().push(context);
},
child: Container(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 28,
backgroundColor: theme.colorScheme.primaryContainer,
child: Icon(
Icons.person,
size: 28,
color: theme.colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 12),
Text(
profile.value?.name ?? 'User',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Tap to switch profile',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
}
/// Extensions section with expandable list of installed extensions.
class _ExtensionsSection extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final addonService = ref.watch(addonServiceProvider);
final pageExtensions = ref.watch(
webExtensionsStateProvider(
WebExtensionActionType.page,
).select((value) => value.values.toList()),
);
final browserExtensions = ref.watch(
webExtensionsStateProvider(
WebExtensionActionType.browser,
).select((value) => value.values.toList()),
);
final isExpanded = useState(false);
return Column(
children: [
ListTile(
leading: const Icon(MdiIcons.puzzle),
title: const Text('Extensions'),
trailing: Icon(
isExpanded.value ? Icons.expand_less : Icons.expand_more,
),
onTap: () => isExpanded.value = !isExpanded.value,
),
if (isExpanded.value)
Padding(
padding: const EdgeInsets.only(left: 16),
child: Column(
children: [
// Page extensions
if (pageExtensions.isNotEmpty) ...[
...pageExtensions.map(
(extension) => ListTile(
leading: ExtensionBadgeIcon(extension),
title: Text(extension.title ?? 'Extension'),
dense: true,
onTap: () async {
Navigator.of(context).pop(); // Close drawer
await addonService.invokeAddonAction(
extension.extensionId,
WebExtensionActionType.page,
);
},
),
),
const Divider(indent: 16, endIndent: 16),
],
// Browser extensions
if (browserExtensions.isNotEmpty) ...[
...browserExtensions.map(
(extension) => ListTile(
leading: ExtensionBadgeIcon(extension),
title: Text(extension.title ?? 'Extension'),
dense: true,
onTap: () async {
Navigator.of(context).pop(); // Close drawer
await addonService.invokeAddonAction(
extension.extensionId,
WebExtensionActionType.browser,
);
},
),
),
const Divider(indent: 16, endIndent: 16),
],
// Management options
ListTile(
leading: const Icon(MdiIcons.puzzleEdit),
title: const Text('Manage Extensions'),
dense: true,
onTap: () async {
Navigator.of(context).pop(); // Close drawer
await addonService.startAddonManagerActivity();
},
),
ListTile(
leading: const Icon(MdiIcons.puzzlePlus),
title: const Text('Get Extensions'),
dense: true,
onTap: () async {
Navigator.of(context).pop(); // Close drawer
final tabMode = TabMode.fromTabType(
ref
.read(generalSettingsWithDefaultsProvider)
.effectiveDefaultCreateTabType,
);
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: Uri.parse('https://addons.mozilla.org'),
tabMode: tabMode,
containerSelection:
const TabContainerSelection.unassigned(),
selectTab: true,
);
},
),
],
),
),
],
);
}
}
/// Sync tile widget shown in navigation drawer when sync is active.
/// Displays sync status and allows manual sync trigger.
class _SyncTile extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
// Only show if sync is active
if (!isAuthenticated) {
return const SizedBox.shrink();
}
final syncInfo = ref.watch(
syncRepositoryProvider.select((value) => value.value?.account),
);
final syncStarted = ref.watch(
syncEventProvider.select(
(value) => value.isLoading || value.value?.$1 == SyncEvent.started,
),
);
final isSyncing = syncStarted || syncInfo?.syncing == true;
final controller = useAnimationController(
duration: const Duration(seconds: 2),
);
useEffect(() {
if (isSyncing) {
unawaited(controller.repeat());
} else {
controller.stop();
controller.reset();
}
return null;
}, [isSyncing]);
return ListTile(
leading: RotationTransition(
turns: Tween<double>(begin: 0, end: -1).animate(controller),
child: const Icon(Icons.sync),
),
title: const Text('Sync Now'),
onTap: () async {
await ref.read(syncRepositoryProvider.notifier).syncNow();
final openedTabs = await ref
.read(syncRepositoryProvider.notifier)
.pollIncomingTabsAndOpen();
if (context.mounted) {
if (openedTabs > 0) {
ui_helper.showOpenedTabsFromAnotherDeviceMessage(
context,
openedTabs,
);
} else {
ui_helper.showInfoMessage(
context,
'Synchronization complete',
duration: const Duration(seconds: 2),
);
}
}
if (context.mounted) {
Navigator.of(context).pop();
}
},
);
}
}
@@ -172,7 +172,6 @@ class _GesturesSection extends StatelessWidget {
children: [
SettingSection(name: 'Gestures'),
_PullToRefreshTile(),
_DrawerGestureTile(),
_DoubleBackCloseTabTile(),
],
);
@@ -509,31 +508,6 @@ class _PullToRefreshTile extends HookConsumerWidget {
}
}
class _DrawerGestureTile extends HookConsumerWidget {
const _DrawerGestureTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final drawerGestureEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.drawerGestureEnabled),
);
return SwitchListTile.adaptive(
title: const Text('Drawer Swipe Gesture'),
subtitle: const Text('Swipe from screen edge to open navigation drawer'),
secondary: const Icon(MdiIcons.gestureSwipe),
value: drawerGestureEnabled,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.drawerGestureEnabled(value),
);
},
);
}
}
class _DoubleBackCloseTabTile extends HookConsumerWidget {
const _DoubleBackCloseTabTile();
@@ -58,7 +58,6 @@ class _AppearanceDisplayTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Appearance & Display'),
@@ -83,7 +82,6 @@ class _PrivacySecurityTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Privacy & Security'),
@@ -108,7 +106,6 @@ class _SearchContentTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Search & Content'),
@@ -133,7 +130,6 @@ class _TabsBehaviorTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Tabs & Behavior'),
@@ -158,7 +154,6 @@ class _FingerprintingTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Fingerprinting'),
@@ -183,7 +178,6 @@ class _SyncTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Firefox Sync'),
@@ -208,7 +202,6 @@ class _AdvancedTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Advanced'),
@@ -91,7 +91,6 @@ class GeneralSettings with FastEquatable {
final bool tabListShowFavicons;
final bool quickTabSwitcherShowTitles;
final bool quickTabSwitcherShowHistorySuggestions;
final bool drawerGestureEnabled;
final String syncServerOverride;
final String syncTokenServerOverride;
final bool urlCleanerEnabled;
@@ -137,7 +136,6 @@ class GeneralSettings with FastEquatable {
required this.tabListShowFavicons,
required this.quickTabSwitcherShowTitles,
required this.quickTabSwitcherShowHistorySuggestions,
required this.drawerGestureEnabled,
required this.syncServerOverride,
required this.syncTokenServerOverride,
required this.urlCleanerEnabled,
@@ -184,7 +182,6 @@ class GeneralSettings with FastEquatable {
bool? tabListShowFavicons,
bool? quickTabSwitcherShowTitles,
bool? quickTabSwitcherShowHistorySuggestions,
bool? drawerGestureEnabled,
String? syncServerOverride,
String? syncTokenServerOverride,
bool? urlCleanerEnabled,
@@ -234,7 +231,6 @@ class GeneralSettings with FastEquatable {
quickTabSwitcherShowTitles = quickTabSwitcherShowTitles ?? true,
quickTabSwitcherShowHistorySuggestions =
quickTabSwitcherShowHistorySuggestions ?? true,
drawerGestureEnabled = drawerGestureEnabled ?? false,
syncServerOverride = syncServerOverride ?? '',
syncTokenServerOverride = syncTokenServerOverride ?? '',
urlCleanerEnabled = urlCleanerEnabled ?? true,
@@ -305,7 +301,6 @@ class GeneralSettings with FastEquatable {
tabListShowFavicons,
quickTabSwitcherShowTitles,
quickTabSwitcherShowHistorySuggestions,
drawerGestureEnabled,
syncServerOverride,
syncTokenServerOverride,
urlCleanerEnabled,
@@ -85,8 +85,6 @@ abstract class _$GeneralSettingsCWProxy {
bool quickTabSwitcherShowHistorySuggestions,
);
GeneralSettings drawerGestureEnabled(bool drawerGestureEnabled);
GeneralSettings syncServerOverride(String syncServerOverride);
GeneralSettings syncTokenServerOverride(String syncTokenServerOverride);
@@ -152,7 +150,6 @@ abstract class _$GeneralSettingsCWProxy {
bool tabListShowFavicons,
bool quickTabSwitcherShowTitles,
bool quickTabSwitcherShowHistorySuggestions,
bool drawerGestureEnabled,
String syncServerOverride,
String syncTokenServerOverride,
bool urlCleanerEnabled,
@@ -309,10 +306,6 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
quickTabSwitcherShowHistorySuggestions,
);
@override
GeneralSettings drawerGestureEnabled(bool drawerGestureEnabled) =>
call(drawerGestureEnabled: drawerGestureEnabled);
@override
GeneralSettings syncServerOverride(String syncServerOverride) =>
call(syncServerOverride: syncServerOverride);
@@ -404,7 +397,6 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
Object? quickTabSwitcherShowTitles = const $CopyWithPlaceholder(),
Object? quickTabSwitcherShowHistorySuggestions =
const $CopyWithPlaceholder(),
Object? drawerGestureEnabled = const $CopyWithPlaceholder(),
Object? syncServerOverride = const $CopyWithPlaceholder(),
Object? syncTokenServerOverride = const $CopyWithPlaceholder(),
Object? urlCleanerEnabled = const $CopyWithPlaceholder(),
@@ -602,12 +594,6 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
? _value.quickTabSwitcherShowHistorySuggestions
// ignore: cast_nullable_to_non_nullable
: quickTabSwitcherShowHistorySuggestions as bool,
drawerGestureEnabled:
drawerGestureEnabled == const $CopyWithPlaceholder() ||
drawerGestureEnabled == null
? _value.drawerGestureEnabled
// ignore: cast_nullable_to_non_nullable
: drawerGestureEnabled as bool,
syncServerOverride:
syncServerOverride == const $CopyWithPlaceholder() ||
syncServerOverride == null
@@ -761,7 +747,6 @@ GeneralSettings _$GeneralSettingsFromJson(
quickTabSwitcherShowTitles: json['quickTabSwitcherShowTitles'] as bool?,
quickTabSwitcherShowHistorySuggestions:
json['quickTabSwitcherShowHistorySuggestions'] as bool?,
drawerGestureEnabled: json['drawerGestureEnabled'] as bool?,
syncServerOverride: json['syncServerOverride'] as String?,
syncTokenServerOverride: json['syncTokenServerOverride'] as String?,
urlCleanerEnabled: json['urlCleanerEnabled'] as bool?,
@@ -823,7 +808,6 @@ Map<String, dynamic> _$GeneralSettingsToJson(
'quickTabSwitcherShowTitles': instance.quickTabSwitcherShowTitles,
'quickTabSwitcherShowHistorySuggestions':
instance.quickTabSwitcherShowHistorySuggestions,
'drawerGestureEnabled': instance.drawerGestureEnabled,
'syncServerOverride': instance.syncServerOverride,
'syncTokenServerOverride': instance.syncTokenServerOverride,
'urlCleanerEnabled': instance.urlCleanerEnabled,
@@ -164,10 +164,6 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.bool,
db.typeMapping,
),
'drawerGestureEnabled': settings['drawerGestureEnabled']?.readAs(
DriftSqlType.bool,
db.typeMapping,
),
'syncServerOverride': settings['syncServerOverride']?.readAs(
DriftSqlType.string,
db.typeMapping,
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
}
String _$generalSettingsRepositoryHash() =>
r'51cfcf20b24705f5c6f51d4e204fff5a8e4eee89';
r'4c2ee264cafd243916e59a78b0d5723997eab823';
abstract class _$GeneralSettingsRepository
extends $StreamNotifier<GeneralSettings> {
+63 -3
View File
@@ -32,6 +32,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'
import 'package:home_widget/home_widget.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logger/logger.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
import 'package:nullability/nullability.dart';
import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/error_observer.dart';
@@ -48,6 +49,38 @@ import 'package:weblibre/features/web_feed/utils/fetch_entrypoint.dart';
import 'package:weblibre/presentation/hooks/on_initialization.dart';
import 'package:weblibre/presentation/main_app.dart';
ColorScheme _fixSurfaceContainerColors(
ColorScheme scheme,
TonalPalette neutralPalette,
Brightness brightness,
) {
if (brightness == Brightness.light) {
return scheme.copyWith(
surfaceContainerLowest: Color(neutralPalette.get(100)),
surfaceContainerLow: Color(neutralPalette.get(96)),
surfaceContainer: Color(neutralPalette.get(94)),
surfaceContainerHigh: Color(neutralPalette.get(92)),
surfaceContainerHighest: Color(neutralPalette.get(90)),
);
} else {
return scheme.copyWith(
surfaceContainerLowest: Color(neutralPalette.get(4)),
surfaceContainerLow: Color(neutralPalette.get(10)),
surfaceContainer: Color(neutralPalette.get(12)),
surfaceContainerHigh: Color(neutralPalette.get(17)),
surfaceContainerHighest: Color(neutralPalette.get(22)),
);
}
}
bool _hasBrokenSurfaceContainerColors(ColorScheme scheme) {
return scheme.surfaceContainerLowest == scheme.surface &&
scheme.surfaceContainerLow == scheme.surface &&
scheme.surfaceContainer == scheme.surface &&
scheme.surfaceContainerHigh == scheme.surface &&
scheme.surfaceContainerHighest == scheme.surface;
}
class _MainWidget extends HookConsumerWidget {
const _MainWidget();
@@ -159,18 +192,45 @@ class _MainWidget extends HookConsumerWidget {
}
});
final corePaletteSnapshot = useFuture(
useMemoized(() => DynamicColorPlugin.getCorePalette()),
);
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
ColorScheme lightColorScheme;
ColorScheme darkColorScheme;
if (lightDynamic != null && darkDynamic != null) {
final corePalette = corePaletteSnapshot.data;
// On Android S+ devices, use the provided dynamic color scheme.
// (Recommended) Harmonize the dynamic color scheme' built-in semantic colors.
lightColorScheme = lightDynamic.harmonized();
final harmonizedLight = lightDynamic.harmonized();
final harmonizedDark = darkDynamic.harmonized();
// Repeat for the dark color scheme.
darkColorScheme = darkDynamic.harmonized();
// Workaround for https://github.com/material-foundation/flutter-packages/issues/649
// dynamic_color package returns broken surfaceContainer* colors.
// Fix them using the neutral tonal palette from CorePalette.
if (corePalette != null) {
lightColorScheme = _hasBrokenSurfaceContainerColors(harmonizedLight)
? _fixSurfaceContainerColors(
harmonizedLight,
corePalette.neutral,
Brightness.light,
)
: harmonizedLight;
darkColorScheme = _hasBrokenSurfaceContainerColors(harmonizedDark)
? _fixSurfaceContainerColors(
harmonizedDark,
corePalette.neutral,
Brightness.dark,
)
: harmonizedDark;
} else {
lightColorScheme = harmonizedLight;
darkColorScheme = harmonizedDark;
}
} else {
// Otherwise, use fallback schemes.
lightColorScheme = ColorScheme.fromSeed(
+1
View File
@@ -56,6 +56,7 @@ dependencies:
path: ../packages/locale_resolver
logger: ^2.6.2
markdown: ^7.3.0
material_color_utilities: ^0.13.0
mime: ^2.0.0
nullability: ^1.0.0
package_info_plus: ^9.0.0
@@ -9,15 +9,21 @@ package eu.weblibre.flutter_mozilla_components.api
import android.content.Context
import android.content.Intent
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.addons.AddonInternalSettingsActivity
import eu.weblibre.flutter_mozilla_components.addons.AddonsActivity
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonsApi
import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionActionType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.concept.engine.webextension.InstallationMethod
class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
private val scope = CoroutineScope(Dispatchers.IO)
override fun startAddonManagerActivity() {
val intent = Intent(context, AddonsActivity::class.java)
@@ -25,6 +31,55 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
context.startActivity(intent)
}
override fun startAddonSettingsActivity(extensionId: String) {
scope.launch {
val addon = runCatching {
components.core.addonManager.getAddons()
.find { it.id == extensionId }
}.getOrNull()
if (addon == null) {
withContext(Dispatchers.Main) {
startAddonManagerActivity()
}
return@launch
}
val optionsPageUrl = addon.installedState?.optionsPageUrl
if (optionsPageUrl.isNullOrEmpty()) {
withContext(Dispatchers.Main) {
startAddonManagerActivity()
}
return@launch
}
withContext(Dispatchers.Main) {
if (addon.installedState?.openOptionsPageInTab == true) {
components.useCases.tabsUseCases.selectOrAddTab(
url = optionsPageUrl,
ignoreFragment = true,
)
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
launchIntent?.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP,
)
if (launchIntent != null) {
context.startActivity(launchIntent)
} else {
startAddonManagerActivity()
}
} else {
val intent = Intent(context, AddonInternalSettingsActivity::class.java)
intent.putExtra("add_on", addon)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
}
}
}
override fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType) {
when(actionType) {
WebExtensionActionType.BROWSER -> components.features.webExtensionToolbarFeature.invokeAddonBrowserAction(extensionId)
@@ -50,4 +105,4 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
}
)
}
}
}
@@ -6815,6 +6815,7 @@ class GeckoSelectionActionEvents(private val binaryMessenger: BinaryMessenger, p
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoAddonsApi {
fun startAddonManagerActivity()
fun startAddonSettingsActivity(extensionId: String)
fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType)
fun installAddon(url: String, callback: (Result<Unit>) -> Unit)
@@ -6843,6 +6844,24 @@ interface GeckoAddonsApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.startAddonSettingsActivity$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val extensionIdArg = args[0] as String
val wrapped: List<Any?> = try {
api.startAddonSettingsActivity(extensionIdArg)
listOf(null)
} catch (exception: Throwable) {
GeckoPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$separatedMessageChannelSuffix", codec)
if (api != null) {
@@ -38,6 +38,10 @@ class GeckoAddonService extends GeckoAddonEvents {
return _api.startAddonManagerActivity();
}
Future<void> startAddonSettingsActivity(String extensionId) {
return _api.startAddonSettingsActivity(extensionId);
}
Future<void> invokeAddonAction(
String extensionId,
WebExtensionActionType actionType,
@@ -8550,6 +8550,31 @@ class GeckoAddonsApi {
}
}
Future<void> startAddonSettingsActivity(String extensionId) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.startAddonSettingsActivity$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[extensionId],
);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
Future<void> invokeAddonAction(
String extensionId,
WebExtensionActionType actionType,
@@ -1602,6 +1602,8 @@ abstract class GeckoSelectionActionEvents {
abstract class GeckoAddonsApi {
void startAddonManagerActivity();
void startAddonSettingsActivity(String extensionId);
void invokeAddonAction(String extensionId, WebExtensionActionType actionType);
@async