merge navigation and tab menu into bottom sheet
This commit is contained in:
+46
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -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_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/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/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/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_grid_view.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_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) {
|
ref.listen(overlayControllerProvider, (previous, next) {
|
||||||
if (next != null) {
|
if (next != null) {
|
||||||
@@ -567,8 +561,6 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
// Minimal scaffold - only for Material overlay support (SnackBars)
|
// Minimal scaffold - only for Material overlay support (SnackBars)
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
endDrawer: const BrowserNavigationDrawer(),
|
|
||||||
endDrawerEnableOpenDragGesture: drawerGestureEnabled,
|
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Layer 0: Browser content
|
// Layer 0: Browser content
|
||||||
@@ -879,6 +871,13 @@ class _Browser extends HookConsumerWidget {
|
|||||||
return false;
|
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) {
|
if (ref.read(bottomSheetControllerProvider) != null) {
|
||||||
ref
|
ref
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
.read(bottomSheetControllerProvider.notifier)
|
||||||
|
|||||||
+1805
File diff suppressed because it is too large
Load Diff
+3
-43
@@ -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/entities/sheet.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.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/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/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/extension_shortcut_menu.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.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/data/models/general_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/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/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/selectable_chips.dart';
|
||||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||||
|
|
||||||
@@ -80,7 +80,6 @@ class BrowserTopAppBar extends HookConsumerWidget {
|
|||||||
showQuickTabSwitcherBar: false,
|
showQuickTabSwitcherBar: false,
|
||||||
showMainToolbarNavigationButton: !showContextualToolbar,
|
showMainToolbarNavigationButton: !showContextualToolbar,
|
||||||
showMainToolbarTabsCount: !showContextualToolbar,
|
showMainToolbarTabsCount: !showContextualToolbar,
|
||||||
showMainToolbarTabActionButton: !showContextualToolbar,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +116,6 @@ class BrowserBottomAppBar extends HookConsumerWidget {
|
|||||||
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
|
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
|
||||||
showMainToolbarNavigationButton: !showContextualToolbar,
|
showMainToolbarNavigationButton: !showContextualToolbar,
|
||||||
showMainToolbarTabsCount: !showContextualToolbar,
|
showMainToolbarTabsCount: !showContextualToolbar,
|
||||||
showMainToolbarTabActionButton: !showContextualToolbar,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +145,6 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
|
|
||||||
final bool showMainToolbarTabsCount;
|
final bool showMainToolbarTabsCount;
|
||||||
final bool showMainToolbarNavigationButton;
|
final bool showMainToolbarNavigationButton;
|
||||||
final bool showMainToolbarTabActionButton;
|
|
||||||
|
|
||||||
const BrowserTabBar({
|
const BrowserTabBar({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -157,7 +154,6 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
required this.showQuickTabSwitcherBar,
|
required this.showQuickTabSwitcherBar,
|
||||||
required this.showMainToolbarTabsCount,
|
required this.showMainToolbarTabsCount,
|
||||||
required this.showMainToolbarNavigationButton,
|
required this.showMainToolbarNavigationButton,
|
||||||
required this.showMainToolbarTabActionButton,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
static const contextualToolabarHeight = 54.0;
|
static const contextualToolabarHeight = 54.0;
|
||||||
@@ -191,7 +187,6 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final extensionMenuController = useMenuController();
|
final extensionMenuController = useMenuController();
|
||||||
final trippleDotMenuController = useMenuController();
|
|
||||||
|
|
||||||
final selectedTabId = ref.watch(selectedTabProvider);
|
final selectedTabId = ref.watch(selectedTabProvider);
|
||||||
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||||
@@ -360,23 +355,6 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
displayedSheet: displayedSheet,
|
displayedSheet: displayedSheet,
|
||||||
showLongPressMenu: true,
|
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,
|
displayedSheet: displayedSheet,
|
||||||
showLongPressMenu: false,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ToolbarButton(
|
return ToolbarButton(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
Scaffold.of(context).openEndDrawer();
|
await showBrowserMenuSheet(context);
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.menu),
|
child: const Icon(Icons.menu),
|
||||||
);
|
);
|
||||||
|
|||||||
-422
@@ -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: [
|
children: [
|
||||||
SettingSection(name: 'Gestures'),
|
SettingSection(name: 'Gestures'),
|
||||||
_PullToRefreshTile(),
|
_PullToRefreshTile(),
|
||||||
_DrawerGestureTile(),
|
|
||||||
_DoubleBackCloseTabTile(),
|
_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 {
|
class _DoubleBackCloseTabTile extends HookConsumerWidget {
|
||||||
const _DoubleBackCloseTabTile();
|
const _DoubleBackCloseTabTile();
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class _AppearanceDisplayTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Theme.of(context).highlightColor,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: const Text('Appearance & Display'),
|
title: const Text('Appearance & Display'),
|
||||||
@@ -83,7 +82,6 @@ class _PrivacySecurityTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Theme.of(context).highlightColor,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: const Text('Privacy & Security'),
|
title: const Text('Privacy & Security'),
|
||||||
@@ -108,7 +106,6 @@ class _SearchContentTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Theme.of(context).highlightColor,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: const Text('Search & Content'),
|
title: const Text('Search & Content'),
|
||||||
@@ -133,7 +130,6 @@ class _TabsBehaviorTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Theme.of(context).highlightColor,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: const Text('Tabs & Behavior'),
|
title: const Text('Tabs & Behavior'),
|
||||||
@@ -158,7 +154,6 @@ class _FingerprintingTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Theme.of(context).highlightColor,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: const Text('Fingerprinting'),
|
title: const Text('Fingerprinting'),
|
||||||
@@ -183,7 +178,6 @@ class _SyncTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Theme.of(context).highlightColor,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: const Text('Firefox Sync'),
|
title: const Text('Firefox Sync'),
|
||||||
@@ -208,7 +202,6 @@ class _AdvancedTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Theme.of(context).highlightColor,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: const Text('Advanced'),
|
title: const Text('Advanced'),
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ class GeneralSettings with FastEquatable {
|
|||||||
final bool tabListShowFavicons;
|
final bool tabListShowFavicons;
|
||||||
final bool quickTabSwitcherShowTitles;
|
final bool quickTabSwitcherShowTitles;
|
||||||
final bool quickTabSwitcherShowHistorySuggestions;
|
final bool quickTabSwitcherShowHistorySuggestions;
|
||||||
final bool drawerGestureEnabled;
|
|
||||||
final String syncServerOverride;
|
final String syncServerOverride;
|
||||||
final String syncTokenServerOverride;
|
final String syncTokenServerOverride;
|
||||||
final bool urlCleanerEnabled;
|
final bool urlCleanerEnabled;
|
||||||
@@ -137,7 +136,6 @@ class GeneralSettings with FastEquatable {
|
|||||||
required this.tabListShowFavicons,
|
required this.tabListShowFavicons,
|
||||||
required this.quickTabSwitcherShowTitles,
|
required this.quickTabSwitcherShowTitles,
|
||||||
required this.quickTabSwitcherShowHistorySuggestions,
|
required this.quickTabSwitcherShowHistorySuggestions,
|
||||||
required this.drawerGestureEnabled,
|
|
||||||
required this.syncServerOverride,
|
required this.syncServerOverride,
|
||||||
required this.syncTokenServerOverride,
|
required this.syncTokenServerOverride,
|
||||||
required this.urlCleanerEnabled,
|
required this.urlCleanerEnabled,
|
||||||
@@ -184,7 +182,6 @@ class GeneralSettings with FastEquatable {
|
|||||||
bool? tabListShowFavicons,
|
bool? tabListShowFavicons,
|
||||||
bool? quickTabSwitcherShowTitles,
|
bool? quickTabSwitcherShowTitles,
|
||||||
bool? quickTabSwitcherShowHistorySuggestions,
|
bool? quickTabSwitcherShowHistorySuggestions,
|
||||||
bool? drawerGestureEnabled,
|
|
||||||
String? syncServerOverride,
|
String? syncServerOverride,
|
||||||
String? syncTokenServerOverride,
|
String? syncTokenServerOverride,
|
||||||
bool? urlCleanerEnabled,
|
bool? urlCleanerEnabled,
|
||||||
@@ -234,7 +231,6 @@ class GeneralSettings with FastEquatable {
|
|||||||
quickTabSwitcherShowTitles = quickTabSwitcherShowTitles ?? true,
|
quickTabSwitcherShowTitles = quickTabSwitcherShowTitles ?? true,
|
||||||
quickTabSwitcherShowHistorySuggestions =
|
quickTabSwitcherShowHistorySuggestions =
|
||||||
quickTabSwitcherShowHistorySuggestions ?? true,
|
quickTabSwitcherShowHistorySuggestions ?? true,
|
||||||
drawerGestureEnabled = drawerGestureEnabled ?? false,
|
|
||||||
syncServerOverride = syncServerOverride ?? '',
|
syncServerOverride = syncServerOverride ?? '',
|
||||||
syncTokenServerOverride = syncTokenServerOverride ?? '',
|
syncTokenServerOverride = syncTokenServerOverride ?? '',
|
||||||
urlCleanerEnabled = urlCleanerEnabled ?? true,
|
urlCleanerEnabled = urlCleanerEnabled ?? true,
|
||||||
@@ -305,7 +301,6 @@ class GeneralSettings with FastEquatable {
|
|||||||
tabListShowFavicons,
|
tabListShowFavicons,
|
||||||
quickTabSwitcherShowTitles,
|
quickTabSwitcherShowTitles,
|
||||||
quickTabSwitcherShowHistorySuggestions,
|
quickTabSwitcherShowHistorySuggestions,
|
||||||
drawerGestureEnabled,
|
|
||||||
syncServerOverride,
|
syncServerOverride,
|
||||||
syncTokenServerOverride,
|
syncTokenServerOverride,
|
||||||
urlCleanerEnabled,
|
urlCleanerEnabled,
|
||||||
|
|||||||
@@ -85,8 +85,6 @@ abstract class _$GeneralSettingsCWProxy {
|
|||||||
bool quickTabSwitcherShowHistorySuggestions,
|
bool quickTabSwitcherShowHistorySuggestions,
|
||||||
);
|
);
|
||||||
|
|
||||||
GeneralSettings drawerGestureEnabled(bool drawerGestureEnabled);
|
|
||||||
|
|
||||||
GeneralSettings syncServerOverride(String syncServerOverride);
|
GeneralSettings syncServerOverride(String syncServerOverride);
|
||||||
|
|
||||||
GeneralSettings syncTokenServerOverride(String syncTokenServerOverride);
|
GeneralSettings syncTokenServerOverride(String syncTokenServerOverride);
|
||||||
@@ -152,7 +150,6 @@ abstract class _$GeneralSettingsCWProxy {
|
|||||||
bool tabListShowFavicons,
|
bool tabListShowFavicons,
|
||||||
bool quickTabSwitcherShowTitles,
|
bool quickTabSwitcherShowTitles,
|
||||||
bool quickTabSwitcherShowHistorySuggestions,
|
bool quickTabSwitcherShowHistorySuggestions,
|
||||||
bool drawerGestureEnabled,
|
|
||||||
String syncServerOverride,
|
String syncServerOverride,
|
||||||
String syncTokenServerOverride,
|
String syncTokenServerOverride,
|
||||||
bool urlCleanerEnabled,
|
bool urlCleanerEnabled,
|
||||||
@@ -309,10 +306,6 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
quickTabSwitcherShowHistorySuggestions,
|
quickTabSwitcherShowHistorySuggestions,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
|
||||||
GeneralSettings drawerGestureEnabled(bool drawerGestureEnabled) =>
|
|
||||||
call(drawerGestureEnabled: drawerGestureEnabled);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GeneralSettings syncServerOverride(String syncServerOverride) =>
|
GeneralSettings syncServerOverride(String syncServerOverride) =>
|
||||||
call(syncServerOverride: syncServerOverride);
|
call(syncServerOverride: syncServerOverride);
|
||||||
@@ -404,7 +397,6 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
Object? quickTabSwitcherShowTitles = const $CopyWithPlaceholder(),
|
Object? quickTabSwitcherShowTitles = const $CopyWithPlaceholder(),
|
||||||
Object? quickTabSwitcherShowHistorySuggestions =
|
Object? quickTabSwitcherShowHistorySuggestions =
|
||||||
const $CopyWithPlaceholder(),
|
const $CopyWithPlaceholder(),
|
||||||
Object? drawerGestureEnabled = const $CopyWithPlaceholder(),
|
|
||||||
Object? syncServerOverride = const $CopyWithPlaceholder(),
|
Object? syncServerOverride = const $CopyWithPlaceholder(),
|
||||||
Object? syncTokenServerOverride = const $CopyWithPlaceholder(),
|
Object? syncTokenServerOverride = const $CopyWithPlaceholder(),
|
||||||
Object? urlCleanerEnabled = const $CopyWithPlaceholder(),
|
Object? urlCleanerEnabled = const $CopyWithPlaceholder(),
|
||||||
@@ -602,12 +594,6 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
? _value.quickTabSwitcherShowHistorySuggestions
|
? _value.quickTabSwitcherShowHistorySuggestions
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: quickTabSwitcherShowHistorySuggestions as bool,
|
: quickTabSwitcherShowHistorySuggestions as bool,
|
||||||
drawerGestureEnabled:
|
|
||||||
drawerGestureEnabled == const $CopyWithPlaceholder() ||
|
|
||||||
drawerGestureEnabled == null
|
|
||||||
? _value.drawerGestureEnabled
|
|
||||||
// ignore: cast_nullable_to_non_nullable
|
|
||||||
: drawerGestureEnabled as bool,
|
|
||||||
syncServerOverride:
|
syncServerOverride:
|
||||||
syncServerOverride == const $CopyWithPlaceholder() ||
|
syncServerOverride == const $CopyWithPlaceholder() ||
|
||||||
syncServerOverride == null
|
syncServerOverride == null
|
||||||
@@ -761,7 +747,6 @@ GeneralSettings _$GeneralSettingsFromJson(
|
|||||||
quickTabSwitcherShowTitles: json['quickTabSwitcherShowTitles'] as bool?,
|
quickTabSwitcherShowTitles: json['quickTabSwitcherShowTitles'] as bool?,
|
||||||
quickTabSwitcherShowHistorySuggestions:
|
quickTabSwitcherShowHistorySuggestions:
|
||||||
json['quickTabSwitcherShowHistorySuggestions'] as bool?,
|
json['quickTabSwitcherShowHistorySuggestions'] as bool?,
|
||||||
drawerGestureEnabled: json['drawerGestureEnabled'] as bool?,
|
|
||||||
syncServerOverride: json['syncServerOverride'] as String?,
|
syncServerOverride: json['syncServerOverride'] as String?,
|
||||||
syncTokenServerOverride: json['syncTokenServerOverride'] as String?,
|
syncTokenServerOverride: json['syncTokenServerOverride'] as String?,
|
||||||
urlCleanerEnabled: json['urlCleanerEnabled'] as bool?,
|
urlCleanerEnabled: json['urlCleanerEnabled'] as bool?,
|
||||||
@@ -823,7 +808,6 @@ Map<String, dynamic> _$GeneralSettingsToJson(
|
|||||||
'quickTabSwitcherShowTitles': instance.quickTabSwitcherShowTitles,
|
'quickTabSwitcherShowTitles': instance.quickTabSwitcherShowTitles,
|
||||||
'quickTabSwitcherShowHistorySuggestions':
|
'quickTabSwitcherShowHistorySuggestions':
|
||||||
instance.quickTabSwitcherShowHistorySuggestions,
|
instance.quickTabSwitcherShowHistorySuggestions,
|
||||||
'drawerGestureEnabled': instance.drawerGestureEnabled,
|
|
||||||
'syncServerOverride': instance.syncServerOverride,
|
'syncServerOverride': instance.syncServerOverride,
|
||||||
'syncTokenServerOverride': instance.syncTokenServerOverride,
|
'syncTokenServerOverride': instance.syncTokenServerOverride,
|
||||||
'urlCleanerEnabled': instance.urlCleanerEnabled,
|
'urlCleanerEnabled': instance.urlCleanerEnabled,
|
||||||
|
|||||||
@@ -164,10 +164,6 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
|
|||||||
DriftSqlType.bool,
|
DriftSqlType.bool,
|
||||||
db.typeMapping,
|
db.typeMapping,
|
||||||
),
|
),
|
||||||
'drawerGestureEnabled': settings['drawerGestureEnabled']?.readAs(
|
|
||||||
DriftSqlType.bool,
|
|
||||||
db.typeMapping,
|
|
||||||
),
|
|
||||||
'syncServerOverride': settings['syncServerOverride']?.readAs(
|
'syncServerOverride': settings['syncServerOverride']?.readAs(
|
||||||
DriftSqlType.string,
|
DriftSqlType.string,
|
||||||
db.typeMapping,
|
db.typeMapping,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$generalSettingsRepositoryHash() =>
|
String _$generalSettingsRepositoryHash() =>
|
||||||
r'51cfcf20b24705f5c6f51d4e204fff5a8e4eee89';
|
r'4c2ee264cafd243916e59a78b0d5723997eab823';
|
||||||
|
|
||||||
abstract class _$GeneralSettingsRepository
|
abstract class _$GeneralSettingsRepository
|
||||||
extends $StreamNotifier<GeneralSettings> {
|
extends $StreamNotifier<GeneralSettings> {
|
||||||
|
|||||||
+63
-3
@@ -32,6 +32,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'
|
|||||||
import 'package:home_widget/home_widget.dart';
|
import 'package:home_widget/home_widget.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:weblibre/core/design/app_colors.dart';
|
import 'package:weblibre/core/design/app_colors.dart';
|
||||||
import 'package:weblibre/core/error_observer.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/hooks/on_initialization.dart';
|
||||||
import 'package:weblibre/presentation/main_app.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 {
|
class _MainWidget extends HookConsumerWidget {
|
||||||
const _MainWidget();
|
const _MainWidget();
|
||||||
|
|
||||||
@@ -159,18 +192,45 @@ class _MainWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final corePaletteSnapshot = useFuture(
|
||||||
|
useMemoized(() => DynamicColorPlugin.getCorePalette()),
|
||||||
|
);
|
||||||
|
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) {
|
builder: (lightDynamic, darkDynamic) {
|
||||||
ColorScheme lightColorScheme;
|
ColorScheme lightColorScheme;
|
||||||
ColorScheme darkColorScheme;
|
ColorScheme darkColorScheme;
|
||||||
|
|
||||||
if (lightDynamic != null && darkDynamic != null) {
|
if (lightDynamic != null && darkDynamic != null) {
|
||||||
|
final corePalette = corePaletteSnapshot.data;
|
||||||
|
|
||||||
// On Android S+ devices, use the provided dynamic color scheme.
|
// On Android S+ devices, use the provided dynamic color scheme.
|
||||||
// (Recommended) Harmonize the dynamic color scheme' built-in semantic colors.
|
// (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.
|
// Workaround for https://github.com/material-foundation/flutter-packages/issues/649
|
||||||
darkColorScheme = darkDynamic.harmonized();
|
// 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 {
|
} else {
|
||||||
// Otherwise, use fallback schemes.
|
// Otherwise, use fallback schemes.
|
||||||
lightColorScheme = ColorScheme.fromSeed(
|
lightColorScheme = ColorScheme.fromSeed(
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ dependencies:
|
|||||||
path: ../packages/locale_resolver
|
path: ../packages/locale_resolver
|
||||||
logger: ^2.6.2
|
logger: ^2.6.2
|
||||||
markdown: ^7.3.0
|
markdown: ^7.3.0
|
||||||
|
material_color_utilities: ^0.13.0
|
||||||
mime: ^2.0.0
|
mime: ^2.0.0
|
||||||
nullability: ^1.0.0
|
nullability: ^1.0.0
|
||||||
package_info_plus: ^9.0.0
|
package_info_plus: ^9.0.0
|
||||||
|
|||||||
+55
@@ -9,15 +9,21 @@ package eu.weblibre.flutter_mozilla_components.api
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
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.addons.AddonsActivity
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonsApi
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonsApi
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionActionType
|
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
|
import mozilla.components.concept.engine.webextension.InstallationMethod
|
||||||
|
|
||||||
class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||||
private val components by lazy {
|
private val components by lazy {
|
||||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||||
}
|
}
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
override fun startAddonManagerActivity() {
|
override fun startAddonManagerActivity() {
|
||||||
val intent = Intent(context, AddonsActivity::class.java)
|
val intent = Intent(context, AddonsActivity::class.java)
|
||||||
@@ -25,6 +31,55 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
|||||||
context.startActivity(intent)
|
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) {
|
override fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType) {
|
||||||
when(actionType) {
|
when(actionType) {
|
||||||
WebExtensionActionType.BROWSER -> components.features.webExtensionToolbarFeature.invokeAddonBrowserAction(extensionId)
|
WebExtensionActionType.BROWSER -> components.features.webExtensionToolbarFeature.invokeAddonBrowserAction(extensionId)
|
||||||
|
|||||||
+19
@@ -6815,6 +6815,7 @@ class GeckoSelectionActionEvents(private val binaryMessenger: BinaryMessenger, p
|
|||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface GeckoAddonsApi {
|
interface GeckoAddonsApi {
|
||||||
fun startAddonManagerActivity()
|
fun startAddonManagerActivity()
|
||||||
|
fun startAddonSettingsActivity(extensionId: String)
|
||||||
fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType)
|
fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType)
|
||||||
fun installAddon(url: String, callback: (Result<Unit>) -> Unit)
|
fun installAddon(url: String, callback: (Result<Unit>) -> Unit)
|
||||||
|
|
||||||
@@ -6843,6 +6844,24 @@ interface GeckoAddonsApi {
|
|||||||
channel.setMessageHandler(null)
|
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 {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ class GeckoAddonService extends GeckoAddonEvents {
|
|||||||
return _api.startAddonManagerActivity();
|
return _api.startAddonManagerActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> startAddonSettingsActivity(String extensionId) {
|
||||||
|
return _api.startAddonSettingsActivity(extensionId);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> invokeAddonAction(
|
Future<void> invokeAddonAction(
|
||||||
String extensionId,
|
String extensionId,
|
||||||
WebExtensionActionType actionType,
|
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(
|
Future<void> invokeAddonAction(
|
||||||
String extensionId,
|
String extensionId,
|
||||||
WebExtensionActionType actionType,
|
WebExtensionActionType actionType,
|
||||||
|
|||||||
@@ -1602,6 +1602,8 @@ abstract class GeckoSelectionActionEvents {
|
|||||||
abstract class GeckoAddonsApi {
|
abstract class GeckoAddonsApi {
|
||||||
void startAddonManagerActivity();
|
void startAddonManagerActivity();
|
||||||
|
|
||||||
|
void startAddonSettingsActivity(String extensionId);
|
||||||
|
|
||||||
void invokeAddonAction(String extensionId, WebExtensionActionType actionType);
|
void invokeAddonAction(String extensionId, WebExtensionActionType actionType);
|
||||||
|
|
||||||
@async
|
@async
|
||||||
|
|||||||
Reference in New Issue
Block a user