diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index f6526aec..cde94816 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -169,7 +169,8 @@ + android:foregroundServiceType="dataSync" + android:exported="false" /> r'e7538b9f0dd43ea64953164c5402e76fde96f5e9'; +String _$chatModelHash() => r'9e44e8fa1d688968841933aac8e13212cf41f4ed'; /// See also [chatModel]. @ProviderFor(chatModel) @@ -22,7 +22,7 @@ final chatModelProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef ChatModelRef = ProviderRef; -String _$embeddingModelHash() => r'5d65c75df195a26adefac5a75f0e8f6b0f1b8590'; +String _$embeddingModelHash() => r'40111a5f08b43afe6edd399ff0e6c022b3387265'; /// See also [embeddingModel]. @ProviderFor(embeddingModel) diff --git a/app/lib/core/providers.dart b/app/lib/core/providers.dart index 9a70f4ff..6b878ce7 100644 --- a/app/lib/core/providers.dart +++ b/app/lib/core/providers.dart @@ -1,5 +1,6 @@ import 'package:go_router/go_router.dart'; import 'package:lensai/core/routing/routes.dart'; +import 'package:lensai/data/models/drag_data.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -12,3 +13,19 @@ GoRouter router(Ref ref) { routes: $appRoutes, ); } + +@Riverpod() +class WillAcceptDrop extends _$WillAcceptDrop { + void setData(DropTargetData data) { + state = data; + } + + void clear() { + state = null; + } + + @override + DropTargetData? build() { + return null; + } +} diff --git a/app/lib/core/providers.g.dart b/app/lib/core/providers.g.dart index a00bc133..9ba8ea2a 100644 --- a/app/lib/core/providers.g.dart +++ b/app/lib/core/providers.g.dart @@ -22,5 +22,21 @@ final routerProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef RouterRef = ProviderRef; +String _$willAcceptDropHash() => r'97b47784ef9101757602b4408d1f545ef2308830'; + +/// See also [WillAcceptDrop]. +@ProviderFor(WillAcceptDrop) +final willAcceptDropProvider = + AutoDisposeNotifierProvider.internal( + WillAcceptDrop.new, + name: r'willAcceptDropProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$willAcceptDropHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$WillAcceptDrop = AutoDisposeNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index 846ad260..4619c5d0 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -13,6 +13,8 @@ import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/ import 'package:lensai/features/geckoview/features/browser/presentation/screens/browser.dart'; import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_list.dart'; import 'package:lensai/features/search/presentation/screens/search.dart'; +import 'package:lensai/features/settings/presentation/screens/browser_hardening.dart'; +import 'package:lensai/features/settings/presentation/screens/browser_hardening_group.dart'; import 'package:lensai/features/settings/presentation/screens/settings.dart'; import 'package:lensai/features/user/presentation/screens/auth.dart'; @@ -180,6 +182,18 @@ class ContainerListRoute extends GoRouteData { @TypedGoRoute( name: 'SettingsRoute', path: '/settings', + routes: [ + TypedGoRoute( + name: 'BrowserHardeningRoute', + path: 'hardening', + routes: [ + TypedGoRoute( + name: 'BrowserHardeningGroupRoute', + path: 'group/:group', + ), + ], + ), + ], ) class SettingsRoute extends GoRouteData { @override @@ -188,6 +202,24 @@ class SettingsRoute extends GoRouteData { } } +class BrowserHardeningRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const BrowserHardeningScreen(); + } +} + +class BrowserHardeningGroupRoute extends GoRouteData { + final String group; + + const BrowserHardeningGroupRoute({required this.group}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BrowserHardeningGroupScreen(groupName: group); + } +} + @TypedGoRoute( name: 'ChatArchiveListRoute', path: '/chat_archive', diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index 1f6b417a..91b3a7c0 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -259,6 +259,20 @@ RouteBase get $settingsRoute => GoRouteData.$route( path: '/settings', name: 'SettingsRoute', factory: $SettingsRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'hardening', + name: 'BrowserHardeningRoute', + factory: $BrowserHardeningRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'group/:group', + name: 'BrowserHardeningGroupRoute', + factory: $BrowserHardeningGroupRouteExtension._fromState, + ), + ], + ), + ], ); extension $SettingsRouteExtension on SettingsRoute { @@ -278,6 +292,44 @@ extension $SettingsRouteExtension on SettingsRoute { void replace(BuildContext context) => context.replace(location); } +extension $BrowserHardeningRouteExtension on BrowserHardeningRoute { + static BrowserHardeningRoute _fromState(GoRouterState state) => + BrowserHardeningRoute(); + + String get location => GoRouteData.$location( + '/settings/hardening', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $BrowserHardeningGroupRouteExtension on BrowserHardeningGroupRoute { + static BrowserHardeningGroupRoute _fromState(GoRouterState state) => + BrowserHardeningGroupRoute( + group: state.pathParameters['group']!, + ); + + String get location => GoRouteData.$location( + '/settings/hardening/group/${Uri.encodeComponent(group)}', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + RouteBase get $chatArchiveListRoute => GoRouteData.$route( path: '/chat_archive', name: 'ChatArchiveListRoute', diff --git a/app/lib/data/models/drag_data.dart b/app/lib/data/models/drag_data.dart new file mode 100644 index 00000000..559dfb89 --- /dev/null +++ b/app/lib/data/models/drag_data.dart @@ -0,0 +1,41 @@ +import 'package:fast_equatable/fast_equatable.dart'; + +sealed class DropTargetData with FastEquatable {} + +final class ContainerDropData extends DropTargetData { + final String tabId; + + ContainerDropData(this.tabId); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [tabId]; +} + +final class DeleteDropData extends DropTargetData { + final String tabId; + + DeleteDropData(this.tabId); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [tabId]; +} + +sealed class DragTargetData with FastEquatable {} + +final class TabDragData extends DragTargetData { + final String tabId; + + TabDragData(this.tabId); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [tabId]; +} diff --git a/app/lib/features/bangs/presentation/widgets/site_search.dart b/app/lib/features/bangs/presentation/widgets/site_search.dart index 740bdc4d..1d10aece 100644 --- a/app/lib/features/bangs/presentation/widgets/site_search.dart +++ b/app/lib/features/bangs/presentation/widgets/site_search.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/providers/search.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; -import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; import 'package:lensai/features/search/domain/providers/search_suggestions.dart'; diff --git a/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.dart b/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.dart index 8d06a953..e8e041f3 100644 --- a/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.dart +++ b/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:exceptions/exceptions.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:langchain/langchain.dart'; diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 0a5682c8..0c2ec0be 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -4,7 +4,9 @@ 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:lensai/core/providers.dart'; import 'package:lensai/core/routing/routes.dart'; +import 'package:lensai/data/models/drag_data.dart'; import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; @@ -16,7 +18,6 @@ import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:lensai/features/geckoview/features/browser/domain/services/create_tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/services/engine_settings.dart'; -import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; @@ -164,7 +165,8 @@ class BrowserScreen extends HookConsumerWidget { // preferredTool: KagiTool.search, // ), // ); - await context.push(SearchRoute().location); + await context + .push(const SearchRoute().location); }, icon: Icon(KagiTool.search.icon), ), @@ -588,13 +590,19 @@ class BrowserScreen extends HookConsumerWidget { ), MenuItemButton( onPressed: () async { - final x = - await context.push(UserAuthRoute().location); - print(x); + await context.push(UserAuthRoute().location); }, leadingIcon: const Icon(Icons.info), child: const Text('Auth'), ), + MenuItemButton( + onPressed: () async { + await context + .push(BrowserHardeningRoute().location); + }, + leadingIcon: const Icon(Icons.info), + child: const Text('Pref'), + ), ], ), ], @@ -603,169 +611,190 @@ class BrowserScreen extends HookConsumerWidget { ); }, ), - body: OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return displayedOverlayDialog!; + body: DragTarget( + onMove: (details) { + ref + .read(willAcceptDropProvider.notifier) + .setData(DeleteDropData(details.data.tabId)); }, - child: Listener( - onPointerDown: (displayedSheet != null) - ? (_) { - ref.read(bottomSheetControllerProvider.notifier).dismiss(); - } - : null, - child: HookConsumer( - builder: (context, ref, child) { - return BackButtonListener( - onBackButtonPressed: () async { - final tabState = (selectedTabId != null) - ? ref.read(tabStateProvider(selectedTabId)) - : null; - - final tabCount = - ref.read(tabListProvider.select((tabs) => tabs.length)); - - //Don't do anything if a child route is active - if (GoRouterState.of(context).topRoute?.path != - BrowserRoute().location) { - return false; - } - - if (displayedSheet != null) { - ref - .read(bottomSheetControllerProvider.notifier) - .dismiss(); - return true; - } - - if (displayedOverlayDialog != null) { - ref - .read(overlayDialogControllerProvider.notifier) - .dismiss(); - return true; - } - - if (tabState?.isFullScreen == true) { - await ref - .read(selectedTabSessionNotifierProvider) - .exitFullscreen(); - return true; - } - - if (tabState?.historyState.canGoBack == true) { - lastBackButtonPress.value = null; - - final controller = ref.read( - tabSessionProvider(tabId: selectedTabId).notifier, - ); - - await controller.goBack(); - return true; - } - - if (lastBackButtonPress.value != null && - DateTime.now().difference(lastBackButtonPress.value!) < - const Duration(seconds: 2)) { - lastBackButtonPress.value = null; - - if (tabState != null && tabCount > 1) { - await ref - .read(tabRepositoryProvider.notifier) - .closeTab(tabState.id); - return true; - } else { - //Mark back as unhandled and navigator will pop - return false; - } - } else { - lastBackButtonPress.value = DateTime.now(); - ScaffoldMessenger.of(context) - ..clearSnackBars() - ..showSnackBar( - SnackBar( - content: (tabCount > 1) - ? const Text( - 'Please click BACK again to close current tab', - ) - : const Text( - 'Please click BACK again to exit app', - ), - ), - ); - - return true; - } - }, - child: Stack( - children: [ - SafeArea( - child: Stack( - children: [ - const BrowserView(), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Consumer( - builder: (context, ref, child) { - final value = ref.watch( - selectedTabStateProvider.select( - (state) { - if (state?.isLoading == true) { - return state?.progress ?? 100; - } - - //When not loading we assumed finished - return 100; - }, - ), - ); - - return Visibility( - visible: value < 100, - child: LinearProgressIndicator( - value: value / 100, - ), - ); - }, - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Consumer( - builder: (context, ref, child) { - final value = ref.watch( - selectedTabStateProvider.select( - (state) => EdgeInsets.only( - bottom: (state?.isLoading == true && - state?.progress != null && - state!.progress < 100) - ? 4.0 - : 0.0, - ), - ), - ); - - return FindInPageWidget(padding: value); - }, - ), - ), - ], - ), - ), - if (displayedSheet != null) - ModalBarrier( - color: Theme.of(context).dialogTheme.barrierColor ?? - Colors.black54, - ), - ], - ), - ); + onLeave: (data) { + ref.read(willAcceptDropProvider.notifier).clear(); + }, + onAcceptWithDetails: (details) async { + await ref + .read(tabRepositoryProvider.notifier) + .closeTab(details.data.tabId); + }, + builder: (context, _, __) { + return OverlayPortal( + controller: overlayController, + overlayChildBuilder: (context) { + return displayedOverlayDialog!; }, - ), - ), + child: Listener( + onPointerDown: (displayedSheet != null) + ? (_) { + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); + } + : null, + child: HookConsumer( + builder: (context, ref, child) { + return BackButtonListener( + onBackButtonPressed: () async { + final tabState = (selectedTabId != null) + ? ref.read(tabStateProvider(selectedTabId)) + : null; + + final tabCount = ref.read( + tabListProvider.select((tabs) => tabs.length)); + + //Don't do anything if a child route is active + if (GoRouterState.of(context).topRoute?.path != + BrowserRoute().location) { + return false; + } + + if (displayedSheet != null) { + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); + return true; + } + + if (displayedOverlayDialog != null) { + ref + .read(overlayDialogControllerProvider.notifier) + .dismiss(); + return true; + } + + if (tabState?.isFullScreen == true) { + await ref + .read(selectedTabSessionNotifierProvider) + .exitFullscreen(); + return true; + } + + if (tabState?.historyState.canGoBack == true) { + lastBackButtonPress.value = null; + + final controller = ref.read( + tabSessionProvider(tabId: selectedTabId).notifier, + ); + + await controller.goBack(); + return true; + } + + if (lastBackButtonPress.value != null && + DateTime.now() + .difference(lastBackButtonPress.value!) < + const Duration(seconds: 2)) { + lastBackButtonPress.value = null; + + if (tabState != null && tabCount > 1) { + await ref + .read(tabRepositoryProvider.notifier) + .closeTab(tabState.id); + return true; + } else { + //Mark back as unhandled and navigator will pop + return false; + } + } else { + lastBackButtonPress.value = DateTime.now(); + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar( + SnackBar( + content: (tabCount > 1) + ? const Text( + 'Please click BACK again to close current tab', + ) + : const Text( + 'Please click BACK again to exit app', + ), + ), + ); + + return true; + } + }, + child: Stack( + children: [ + SafeArea( + child: Stack( + children: [ + const BrowserView(), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Consumer( + builder: (context, ref, child) { + final value = ref.watch( + selectedTabStateProvider.select( + (state) { + if (state?.isLoading == true) { + return state?.progress ?? 100; + } + + //When not loading we assumed finished + return 100; + }, + ), + ); + + return Visibility( + visible: value < 100, + child: LinearProgressIndicator( + value: value / 100, + ), + ); + }, + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Consumer( + builder: (context, ref, child) { + final value = ref.watch( + selectedTabStateProvider.select( + (state) => EdgeInsets.only( + bottom: (state?.isLoading == true && + state?.progress != null && + state!.progress < 100) + ? 4.0 + : 0.0, + ), + ), + ); + + return FindInPageWidget(padding: value); + }, + ), + ), + ], + ), + ), + if (displayedSheet != null) + ModalBarrier( + color: + Theme.of(context).dialogTheme.barrierColor ?? + Colors.black54, + ), + ], + ), + ); + }, + ), + ), + ); + }, ), floatingActionButton: ReaderAppearanceButton(), bottomSheet: (displayedSheet != null) diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart index c5235032..2636ea33 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart @@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/kagi/presentation/tabs/search_tab.dart'; import 'package:lensai/features/kagi/presentation/tabs/summarize_tab.dart'; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; import 'package:lensai/presentation/hooks/sync_page_tab.dart'; diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart index adfb42a5..d49b50c4 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart @@ -3,24 +3,74 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_reorderable_grid_view/widgets/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/core/providers.dart'; +import 'package:lensai/data/models/drag_data.dart'; import 'package:lensai/data/models/equatable_iterable.dart'; +import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/tab_action.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; -import 'package:lensai/presentation/widgets/speech_to_text_button.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tab_preview.dart'; -import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_chips.dart'; import 'package:lensai/presentation/hooks/listenable_callback.dart'; -import 'package:reorderable_grid/reorderable_grid.dart'; +import 'package:lensai/presentation/widgets/speech_to_text_button.dart'; + +class _TabDraggable extends HookConsumerWidget { + final String tabId; + final String? activeTabId; + + final void Function() onClose; + + _TabDraggable({ + required this.tabId, + required this.activeTabId, + required this.onClose, + }) : super(key: ValueKey(tabId)); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tab = ref.watch(tabStateProvider(tabId)); + + if (tab == null) { + return const SizedBox.shrink(); + } + + return TabPreview( + tab: tab, + isActive: tabId == activeTabId, + onTap: () async { + if (tabId != activeTabId) { + //Close first to avoid rebuilds + onClose(); + await ref.read(tabRepositoryProvider.notifier).selectTab(tab.id); + } else { + onClose(); + } + }, + onDoubleTap: () { + ref.read(overlayDialogControllerProvider.notifier).show( + TabActionDialog( + initialTab: tab, + onDismiss: + ref.read(overlayDialogControllerProvider.notifier).dismiss, + ), + ); + }, + onDelete: () async { + await ref.read(tabRepositoryProvider.notifier).closeTab(tab.id); + }, + ); + } +} class _Tab extends HookConsumerWidget { static const headerSize = 124.0; @@ -194,133 +244,164 @@ class ViewTabsSheetWidget extends HookConsumerWidget { child: _Tab(onClose: onClose), ), Expanded( - child: CustomScrollView( - controller: sheetScrollController, - slivers: [ - HookConsumer( - builder: (context, ref, child) { - final container = ref.watch(selectedContainerProvider); + child: HookConsumer( + builder: (context, ref, child) { + final container = ref.watch(selectedContainerProvider); - final filteredTabs = ref - .watch( - seamlessFilteredTabsProvider(container).select( - (value) => - EquatableCollection(value, immutable: true), + final filteredTabs = ref + .watch( + seamlessFilteredTabsProvider(container).select( + (value) => + EquatableCollection(value, immutable: true), + ), + ) + .collection; + + final activeTab = ref.watch(selectedTabProvider); + + final itemHeight = useMemoized( + () => _calculateItemHeight( + screenWidth: MediaQuery.of(context).size.width, + childAspectRatio: 0.75, + horizontalPadding: 4.0, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + crossAxisCount: 2, + ), + [MediaQuery.of(context).size.width], + ); + + useEffect( + () { + final index = filteredTabs + .indexWhere((webView) => webView == activeTab); + + if (index > -1) { + final offset = (index ~/ 2) * itemHeight; + + if (offset != sheetScrollController.offset) { + unawaited( + sheetScrollController.animateTo( + offset, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ), + ); + } + } + + return null; + }, + [filteredTabs, activeTab], + ); + + final tabs = useMemoized( + () { + return filteredTabs + .mapIndexed( + (index, tabId) => CustomDraggable( + key: Key(tabId), + data: TabDragData(tabId), + child: Consumer( + child: _TabDraggable( + tabId: tabId, + activeTabId: activeTab, + onClose: onClose, + ), + builder: (context, ref, child) { + final dragData = ref.watch( + willAcceptDropProvider.select((value) { + final dragTabId = switch (value) { + ContainerDropData() => value.tabId, + DeleteDropData() => value.tabId, + null => null, + }; + + return (dragTabId == tabId) + ? value + : null; + }), + ); + + return switch (dragData) { + ContainerDropData() => Opacity( + opacity: 0.3, + child: Transform.scale( + scale: 0.9, + child: child, + ), + ), + DeleteDropData() => Opacity( + opacity: 0.3, + child: ColorFiltered( + colorFilter: const ColorFilter.mode( + Colors.red, + BlendMode.modulate, + ), + child: child, + ), + ), + null => child!, + }; + }, + ), ), ) - .collection; + .toList(); + }, + [ + EquatableCollection(filteredTabs, immutable: true), + activeTab, + ], + ); - final activeTab = ref.watch(selectedTabProvider); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: ReorderableBuilder( + scrollController: sheetScrollController, + children: tabs, + onDragStarted: (index) { + ref.read(willAcceptDropProvider.notifier).clear(); + }, + onReorderPositions: (positions) async { + assert( + positions.length == 1, + 'Not ready for multiple reorders', + ); - final itemHeight = useMemoized( - () => _calculateItemHeight( - screenWidth: MediaQuery.of(context).size.width, - childAspectRatio: 0.75, - horizontalPadding: 4.0, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - crossAxisCount: 2, - ), - [MediaQuery.of(context).size.width], - ); + final oldIndex = positions.first.oldIndex; + final newIndex = positions.first.newIndex; - useEffect( - () { - final index = filteredTabs - .indexWhere((webView) => webView == activeTab); + final containerRepository = + ref.read(containerRepositoryProvider.notifier); - if (index > -1) { - final offset = (index ~/ 2) * itemHeight; + final tabId = filteredTabs[oldIndex]; + final containerId = await ref + .read(tabDataRepositoryProvider.notifier) + .containerTabId(tabId); - if (offset != sheetScrollController.offset) { - unawaited( - sheetScrollController.animateTo( - offset, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - ), - ); - } - } + final String key; + if (newIndex <= 0) { + key = await containerRepository + .getLeadingOrderKey(containerId); + } else if (newIndex >= filteredTabs.length - 1) { + key = await containerRepository + .getTrailingOrderKey(containerId); + } else { + final orderAfterIndex = newIndex; + key = await containerRepository.getOrderKeyAfterTab( + filteredTabs[orderAfterIndex], + containerId, + ); + } - return null; - }, - [filteredTabs, activeTab], - ); - - final tabs = useMemoized( - () { - return filteredTabs - .mapIndexed( - (index, tabId) => - ReorderableGridDelayedDragStartListener( - key: ValueKey(tabId), - index: index, - child: Consumer( - builder: (context, ref, child) { - final tab = - ref.watch(tabStateProvider(tabId)); - return (tab != null) - ? TabPreview( - tab: tab, - isActive: tabId == activeTab, - onTap: () async { - if (tabId != activeTab) { - //Close first to avoid rebuilds - onClose(); - await ref - .read( - tabRepositoryProvider - .notifier, - ) - .selectTab(tab.id); - } else { - onClose(); - } - }, - onDoubleTap: () { - ref - .read( - overlayDialogControllerProvider - .notifier, - ) - .show( - TabActionDialog( - initialTab: tab, - onDismiss: ref - .read( - overlayDialogControllerProvider - .notifier, - ) - .dismiss, - ), - ); - }, - onDelete: () async { - await ref - .read( - tabRepositoryProvider - .notifier, - ) - .closeTab(tab.id); - }, - ) - : const SizedBox.shrink(); - }, - ), - ), - ) - .toList(); - }, - [ - EquatableCollection(filteredTabs, immutable: true), - activeTab, - ], - ); - - return SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - sliver: SliverReorderableGrid( + await ref + .read(tabDataRepositoryProvider.notifier) + .assignOrderKey(tabId, key); + }, + builder: (children) { + return GridView.builder( + controller: sheetScrollController, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( //Sync values for itemHeight calculation _calculateItemHeight @@ -329,42 +410,13 @@ class ViewTabsSheetWidget extends HookConsumerWidget { crossAxisSpacing: 8.0, crossAxisCount: 2, ), - itemCount: tabs.length, - itemBuilder: (context, index) => tabs[index], - onReorder: (oldIndex, newIndex) async { - final containerRepository = - ref.read(containerRepositoryProvider.notifier); - - final tabId = filteredTabs[oldIndex]; - final containerId = await ref - .read(tabDataRepositoryProvider.notifier) - .containerTabId(tabId); - - final String key; - if (newIndex <= 0) { - key = await containerRepository - .getLeadingOrderKey(containerId); - } else if (newIndex >= filteredTabs.length - 1) { - key = await containerRepository - .getTrailingOrderKey(containerId); - } else { - final orderAfterIndex = newIndex; - key = - await containerRepository.getOrderKeyAfterTab( - filteredTabs[orderAfterIndex], - containerId, - ); - } - - await ref - .read(tabDataRepositoryProvider.notifier) - .assignOrderKey(tabId, key); - }, - ), - ); - }, - ), - ], + itemCount: children.length, + itemBuilder: (context, index) => children[index], + ); + }, + ), + ); + }, ), ), ], @@ -376,13 +428,14 @@ class ViewTabsSheetWidget extends HookConsumerWidget { ), child: FloatingActionButton.small( onPressed: () async { - await ref - .read(tabRepositoryProvider.notifier) - .addTab(url: Uri.https('kagi.com')); + await ref.read(tabRepositoryProvider.notifier).addTab( + url: Uri.https('setcookie.net'), + // contextId: uuid.v4(), + ); onClose(); }, - child: Icon(Icons.add), + child: const Icon(Icons.add), ), ), ], diff --git a/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart new file mode 100644 index 00000000..cf1326dc --- /dev/null +++ b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart @@ -0,0 +1,75 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'preference_setting.g.dart'; + +@CopyWith() +class PreferenceSettingGroup with FastEquatable { + final String? description; + + final Map settings; + + bool get isActive => settings.values + .every((setting) => setting.requireUserOptIn || setting.isActive); + + bool get isPartlyActive => settings.values.any((setting) => setting.isActive); + + PreferenceSettingGroup({ + required this.description, + required this.settings, + }); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [ + description, + settings, + ]; +} + +@JsonSerializable() +@CopyWith() +class PreferenceSetting with FastEquatable { + final Object value; + + @JsonKey(includeFromJson: false, includeToJson: false) + final Object? actualValue; + + bool get isActive => value == actualValue; + + final String title; + final String? description; + + final bool requireUserOptIn; + final bool shouldBeDefault; + + PreferenceSetting({ + required this.value, + required this.title, + required this.description, + this.actualValue, + this.requireUserOptIn = false, + this.shouldBeDefault = false, + }); + + factory PreferenceSetting.fromJson(Map json) => + _$PreferenceSettingFromJson(json); + + Map toJson() => _$PreferenceSettingToJson(this); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [ + value, + actualValue, + title, + description, + requireUserOptIn, + shouldBeDefault, + ]; +} diff --git a/app/lib/features/geckoview/features/preferences/data/models/preference_setting.g.dart b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.g.dart new file mode 100644 index 00000000..ca70e1cd --- /dev/null +++ b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.g.dart @@ -0,0 +1,202 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'preference_setting.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$PreferenceSettingGroupCWProxy { + PreferenceSettingGroup description(String? description); + + PreferenceSettingGroup settings(Map settings); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PreferenceSettingGroup(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// PreferenceSettingGroup(...).copyWith(id: 12, name: "My name") + /// ```` + PreferenceSettingGroup call({ + String? description, + Map settings, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfPreferenceSettingGroup.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfPreferenceSettingGroup.copyWith.fieldName(...)` +class _$PreferenceSettingGroupCWProxyImpl + implements _$PreferenceSettingGroupCWProxy { + const _$PreferenceSettingGroupCWProxyImpl(this._value); + + final PreferenceSettingGroup _value; + + @override + PreferenceSettingGroup description(String? description) => + this(description: description); + + @override + PreferenceSettingGroup settings(Map settings) => + this(settings: settings); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PreferenceSettingGroup(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// PreferenceSettingGroup(...).copyWith(id: 12, name: "My name") + /// ```` + PreferenceSettingGroup call({ + Object? description = const $CopyWithPlaceholder(), + Object? settings = const $CopyWithPlaceholder(), + }) { + return PreferenceSettingGroup( + description: description == const $CopyWithPlaceholder() + ? _value.description + // ignore: cast_nullable_to_non_nullable + : description as String?, + settings: settings == const $CopyWithPlaceholder() + ? _value.settings + // ignore: cast_nullable_to_non_nullable + : settings as Map, + ); + } +} + +extension $PreferenceSettingGroupCopyWith on PreferenceSettingGroup { + /// Returns a callable class that can be used as follows: `instanceOfPreferenceSettingGroup.copyWith(...)` or like so:`instanceOfPreferenceSettingGroup.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$PreferenceSettingGroupCWProxy get copyWith => + _$PreferenceSettingGroupCWProxyImpl(this); +} + +abstract class _$PreferenceSettingCWProxy { + PreferenceSetting value(Object value); + + PreferenceSetting title(String title); + + PreferenceSetting description(String? description); + + PreferenceSetting actualValue(Object? actualValue); + + PreferenceSetting requireUserOptIn(bool requireUserOptIn); + + PreferenceSetting shouldBeDefault(bool shouldBeDefault); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PreferenceSetting(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// PreferenceSetting(...).copyWith(id: 12, name: "My name") + /// ```` + PreferenceSetting call({ + Object value, + String title, + String? description, + Object? actualValue, + bool requireUserOptIn, + bool shouldBeDefault, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfPreferenceSetting.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfPreferenceSetting.copyWith.fieldName(...)` +class _$PreferenceSettingCWProxyImpl implements _$PreferenceSettingCWProxy { + const _$PreferenceSettingCWProxyImpl(this._value); + + final PreferenceSetting _value; + + @override + PreferenceSetting value(Object value) => this(value: value); + + @override + PreferenceSetting title(String title) => this(title: title); + + @override + PreferenceSetting description(String? description) => + this(description: description); + + @override + PreferenceSetting actualValue(Object? actualValue) => + this(actualValue: actualValue); + + @override + PreferenceSetting requireUserOptIn(bool requireUserOptIn) => + this(requireUserOptIn: requireUserOptIn); + + @override + PreferenceSetting shouldBeDefault(bool shouldBeDefault) => + this(shouldBeDefault: shouldBeDefault); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PreferenceSetting(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// PreferenceSetting(...).copyWith(id: 12, name: "My name") + /// ```` + PreferenceSetting call({ + Object? value = const $CopyWithPlaceholder(), + Object? title = const $CopyWithPlaceholder(), + Object? description = const $CopyWithPlaceholder(), + Object? actualValue = const $CopyWithPlaceholder(), + Object? requireUserOptIn = const $CopyWithPlaceholder(), + Object? shouldBeDefault = const $CopyWithPlaceholder(), + }) { + return PreferenceSetting( + value: value == const $CopyWithPlaceholder() + ? _value.value + // ignore: cast_nullable_to_non_nullable + : value as Object, + title: title == const $CopyWithPlaceholder() + ? _value.title + // ignore: cast_nullable_to_non_nullable + : title as String, + description: description == const $CopyWithPlaceholder() + ? _value.description + // ignore: cast_nullable_to_non_nullable + : description as String?, + actualValue: actualValue == const $CopyWithPlaceholder() + ? _value.actualValue + // ignore: cast_nullable_to_non_nullable + : actualValue as Object?, + requireUserOptIn: requireUserOptIn == const $CopyWithPlaceholder() + ? _value.requireUserOptIn + // ignore: cast_nullable_to_non_nullable + : requireUserOptIn as bool, + shouldBeDefault: shouldBeDefault == const $CopyWithPlaceholder() + ? _value.shouldBeDefault + // ignore: cast_nullable_to_non_nullable + : shouldBeDefault as bool, + ); + } +} + +extension $PreferenceSettingCopyWith on PreferenceSetting { + /// Returns a callable class that can be used as follows: `instanceOfPreferenceSetting.copyWith(...)` or like so:`instanceOfPreferenceSetting.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$PreferenceSettingCWProxy get copyWith => + _$PreferenceSettingCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PreferenceSetting _$PreferenceSettingFromJson(Map json) => + PreferenceSetting( + value: json['value'] as Object, + title: json['title'] as String, + description: json['description'] as String?, + requireUserOptIn: json['requireUserOptIn'] as bool? ?? false, + shouldBeDefault: json['shouldBeDefault'] as bool? ?? false, + ); + +Map _$PreferenceSettingToJson(PreferenceSetting instance) => + { + 'value': instance.value, + 'title': instance.title, + 'description': instance.description, + 'requireUserOptIn': instance.requireUserOptIn, + 'shouldBeDefault': instance.shouldBeDefault, + }; diff --git a/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart b/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart new file mode 100644 index 00000000..9a190742 --- /dev/null +++ b/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart @@ -0,0 +1,193 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/features/geckoview/features/preferences/data/models/preference_setting.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:rxdart/rxdart.dart'; + +part 'preference_settings.g.dart'; + +@Riverpod(keepAlive: true) +Future> _preferenceSettingGroups( + Ref ref, +) async { + final content = await rootBundle + .loadString('assets/preferences/settings.json') + .then(jsonDecode) as Map; + + final userContent = content['user'] as Map; + + return userContent.map( + (key, value) => MapEntry( + key, + PreferenceSettingGroup( + // ignore: avoid_dynamic_calls + description: value['description'] as String?, + // ignore: avoid_dynamic_calls + settings: (value['preferences'] as Map).map( + (key, value) => MapEntry( + key, + PreferenceSetting.fromJson(value as Map), + ), + ), + ), + ), + ); +} + +@Riverpod(keepAlive: true) +Future _preferenceSettingGroup( + Ref ref, + String groupName, +) { + return ref.watch( + _preferenceSettingGroupsProvider.selectAsync( + (groups) => + groups[groupName] ?? (throw Exception('Unknown setting group')), + ), + ); +} + +@Riverpod() +class _PreferenceRepository extends _$PreferenceRepository { + final _prefManager = GeckoPrefService(); + // Use BehaviorSubject instead of StreamController + late BehaviorSubject> _prefSubject; + + Future _updatePrefs() async { + final prefs = await _prefManager.getAllPrefs(); + _prefSubject.add(prefs); + } + + Future applyPrefs(Map prefs) async { + await _prefManager.applyPrefs(prefs); + await _updatePrefs(); + } + + Future resetPrefs(List prefNames) async { + await _prefManager.resetPrefs(prefNames); + await _updatePrefs(); + } + + @override + Raw>> build() { + _prefSubject = BehaviorSubject>(); + + ref.onDispose(() { + unawaited(_prefSubject.close()); + }); + + ref.onAddListener(() async { + await _updatePrefs(); + }); + + return _prefSubject.stream; + } +} + +@Riverpod() +class PreferenceSettingsGeneralRepository + extends _$PreferenceSettingsGeneralRepository { + late Map _statelessGroups; + + Future apply() async { + final prefs = { + for (final group in _statelessGroups.values) + ...Map.fromEntries( + group.settings.entries + .where((e) => !e.value.requireUserOptIn) + .map((e) => MapEntry(e.key, e.value.value)), + ), + }; + + await ref.read(_preferenceRepositoryProvider.notifier).applyPrefs(prefs); + } + + Future reset() async { + final prefs = _statelessGroups.values + .map((group) => group.settings.keys) + .flattened + .toList(); + + await ref.read(_preferenceRepositoryProvider.notifier).resetPrefs(prefs); + } + + @override + Stream> build() async* { + _statelessGroups = await ref.watch(_preferenceSettingGroupsProvider.future); + + final prefStream = ref.watch(_preferenceRepositoryProvider); + + yield* prefStream.map( + (prefs) => _statelessGroups.map( + (groupName, group) => MapEntry( + groupName, + group.copyWith.settings( + group.settings.map( + (prefName, setting) => MapEntry( + prefName, + setting.copyWith.actualValue(prefs[prefName]), + ), + ), + ), + ), + ), + ); + } +} + +@Riverpod() +class PreferenceSettingsGroupRepository + extends _$PreferenceSettingsGroupRepository { + late PreferenceSettingGroup _statelessSettingGroup; + + Future apply({List? filter}) async { + final prefs = Map.fromEntries( + filter?.map( + (e) => MapEntry( + e, + _statelessSettingGroup.settings[e]?.value ?? + (throw Exception('Preference not part of group')), + ), + ) ?? + _statelessSettingGroup.settings.entries + .where((e) => !e.value.requireUserOptIn) + .map((e) => MapEntry(e.key, e.value.value)), + ); + + await ref.read(_preferenceRepositoryProvider.notifier).applyPrefs(prefs); + } + + Future reset({List? filter}) async { + if (filter != null && + filter.any( + (value) => !_statelessSettingGroup.settings.containsKey(value), + )) { + throw Exception('Preference not part of group'); + } + + await ref + .read(_preferenceRepositoryProvider.notifier) + .resetPrefs(filter ?? _statelessSettingGroup.settings.keys.toList()); + } + + @override + Stream build(String groupName) async* { + _statelessSettingGroup = + await ref.watch(_preferenceSettingGroupProvider(groupName).future); + + final prefStream = ref.watch(_preferenceRepositoryProvider); + + yield* prefStream.map( + (prefs) => _statelessSettingGroup.copyWith.settings( + _statelessSettingGroup.settings.map( + (key, value) => MapEntry(key, value.copyWith.actualValue(prefs[key])), + ), + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.g.dart b/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.g.dart new file mode 100644 index 00000000..5b34e468 --- /dev/null +++ b/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.g.dart @@ -0,0 +1,378 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'preference_settings.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$preferenceSettingGroupsHash() => + r'fa9286248cc71c88c888a518441721dc99b7e0c2'; + +/// See also [_preferenceSettingGroups]. +@ProviderFor(_preferenceSettingGroups) +final _preferenceSettingGroupsProvider = + FutureProvider>.internal( + _preferenceSettingGroups, + name: r'_preferenceSettingGroupsProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$preferenceSettingGroupsHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef _PreferenceSettingGroupsRef + = FutureProviderRef>; +String _$preferenceSettingGroupHash() => + r'960169ab9f6985a5b73a63663e671a8eac7cd776'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [_preferenceSettingGroup]. +@ProviderFor(_preferenceSettingGroup) +const _preferenceSettingGroupProvider = _PreferenceSettingGroupFamily(); + +/// See also [_preferenceSettingGroup]. +class _PreferenceSettingGroupFamily + extends Family> { + /// See also [_preferenceSettingGroup]. + const _PreferenceSettingGroupFamily(); + + /// See also [_preferenceSettingGroup]. + _PreferenceSettingGroupProvider call( + String groupName, + ) { + return _PreferenceSettingGroupProvider( + groupName, + ); + } + + @override + _PreferenceSettingGroupProvider getProviderOverride( + covariant _PreferenceSettingGroupProvider provider, + ) { + return call( + provider.groupName, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_preferenceSettingGroupProvider'; +} + +/// See also [_preferenceSettingGroup]. +class _PreferenceSettingGroupProvider + extends FutureProvider { + /// See also [_preferenceSettingGroup]. + _PreferenceSettingGroupProvider( + String groupName, + ) : this._internal( + (ref) => _preferenceSettingGroup( + ref as _PreferenceSettingGroupRef, + groupName, + ), + from: _preferenceSettingGroupProvider, + name: r'_preferenceSettingGroupProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$preferenceSettingGroupHash, + dependencies: _PreferenceSettingGroupFamily._dependencies, + allTransitiveDependencies: + _PreferenceSettingGroupFamily._allTransitiveDependencies, + groupName: groupName, + ); + + _PreferenceSettingGroupProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.groupName, + }) : super.internal(); + + final String groupName; + + @override + Override overrideWith( + FutureOr Function( + _PreferenceSettingGroupRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: _PreferenceSettingGroupProvider._internal( + (ref) => create(ref as _PreferenceSettingGroupRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + groupName: groupName, + ), + ); + } + + @override + FutureProviderElement createElement() { + return _PreferenceSettingGroupProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is _PreferenceSettingGroupProvider && + other.groupName == groupName; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, groupName.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin _PreferenceSettingGroupRef on FutureProviderRef { + /// The parameter `groupName` of this provider. + String get groupName; +} + +class _PreferenceSettingGroupProviderElement + extends FutureProviderElement + with _PreferenceSettingGroupRef { + _PreferenceSettingGroupProviderElement(super.provider); + + @override + String get groupName => (origin as _PreferenceSettingGroupProvider).groupName; +} + +String _$preferenceRepositoryHash() => + r'53c0cbbf60e44c3834fe3e3255836f7e27f09c7f'; + +/// See also [_PreferenceRepository]. +@ProviderFor(_PreferenceRepository) +final _preferenceRepositoryProvider = AutoDisposeNotifierProvider< + _PreferenceRepository, Raw>>>.internal( + _PreferenceRepository.new, + name: r'_preferenceRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$preferenceRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PreferenceRepository + = AutoDisposeNotifier>>>; +String _$preferenceSettingsGeneralRepositoryHash() => + r'24a70de8284107bb43778e1014abe41bd805987a'; + +/// See also [PreferenceSettingsGeneralRepository]. +@ProviderFor(PreferenceSettingsGeneralRepository) +final preferenceSettingsGeneralRepositoryProvider = + AutoDisposeStreamNotifierProvider>.internal( + PreferenceSettingsGeneralRepository.new, + name: r'preferenceSettingsGeneralRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$preferenceSettingsGeneralRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PreferenceSettingsGeneralRepository + = AutoDisposeStreamNotifier>; +String _$preferenceSettingsGroupRepositoryHash() => + r'023794294c5639ac6c6941f87a4ed91e1551fbc9'; + +abstract class _$PreferenceSettingsGroupRepository + extends BuildlessAutoDisposeStreamNotifier { + late final String groupName; + + Stream build( + String groupName, + ); +} + +/// See also [PreferenceSettingsGroupRepository]. +@ProviderFor(PreferenceSettingsGroupRepository) +const preferenceSettingsGroupRepositoryProvider = + PreferenceSettingsGroupRepositoryFamily(); + +/// See also [PreferenceSettingsGroupRepository]. +class PreferenceSettingsGroupRepositoryFamily + extends Family> { + /// See also [PreferenceSettingsGroupRepository]. + const PreferenceSettingsGroupRepositoryFamily(); + + /// See also [PreferenceSettingsGroupRepository]. + PreferenceSettingsGroupRepositoryProvider call( + String groupName, + ) { + return PreferenceSettingsGroupRepositoryProvider( + groupName, + ); + } + + @override + PreferenceSettingsGroupRepositoryProvider getProviderOverride( + covariant PreferenceSettingsGroupRepositoryProvider provider, + ) { + return call( + provider.groupName, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'preferenceSettingsGroupRepositoryProvider'; +} + +/// See also [PreferenceSettingsGroupRepository]. +class PreferenceSettingsGroupRepositoryProvider + extends AutoDisposeStreamNotifierProviderImpl< + PreferenceSettingsGroupRepository, PreferenceSettingGroup> { + /// See also [PreferenceSettingsGroupRepository]. + PreferenceSettingsGroupRepositoryProvider( + String groupName, + ) : this._internal( + () => PreferenceSettingsGroupRepository()..groupName = groupName, + from: preferenceSettingsGroupRepositoryProvider, + name: r'preferenceSettingsGroupRepositoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$preferenceSettingsGroupRepositoryHash, + dependencies: PreferenceSettingsGroupRepositoryFamily._dependencies, + allTransitiveDependencies: PreferenceSettingsGroupRepositoryFamily + ._allTransitiveDependencies, + groupName: groupName, + ); + + PreferenceSettingsGroupRepositoryProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.groupName, + }) : super.internal(); + + final String groupName; + + @override + Stream runNotifierBuild( + covariant PreferenceSettingsGroupRepository notifier, + ) { + return notifier.build( + groupName, + ); + } + + @override + Override overrideWith(PreferenceSettingsGroupRepository Function() create) { + return ProviderOverride( + origin: this, + override: PreferenceSettingsGroupRepositoryProvider._internal( + () => create()..groupName = groupName, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + groupName: groupName, + ), + ); + } + + @override + AutoDisposeStreamNotifierProviderElement createElement() { + return _PreferenceSettingsGroupRepositoryProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is PreferenceSettingsGroupRepositoryProvider && + other.groupName == groupName; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, groupName.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin PreferenceSettingsGroupRepositoryRef + on AutoDisposeStreamNotifierProviderRef { + /// The parameter `groupName` of this provider. + String get groupName; +} + +class _PreferenceSettingsGroupRepositoryProviderElement + extends AutoDisposeStreamNotifierProviderElement< + PreferenceSettingsGroupRepository, + PreferenceSettingGroup> with PreferenceSettingsGroupRepositoryRef { + _PreferenceSettingsGroupRepositoryProviderElement(super.provider); + + @override + String get groupName => + (origin as PreferenceSettingsGroupRepositoryProvider).groupName; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.dart b/app/lib/features/geckoview/features/tabs/data/database/database.dart index 76486aff..1c07abff 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/database.dart @@ -9,9 +9,7 @@ import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_re import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart'; import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/database.dart'; import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart'; -import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart'; import 'package:lensai/features/search/domain/fts_tokenizer.dart'; -import 'package:lensai/features/search/domain/unix_tokenizer.dart'; part 'database.g.dart'; diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart index dcff4992..8e64e81e 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart @@ -2,9 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/core/providers.dart'; import 'package:lensai/core/routing/routes.dart'; +import 'package:lensai/data/models/drag_data.dart'; import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart'; +import 'package:lensai/presentation/hooks/overlay_portal_controller.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; class ContainerChips extends HookConsumerWidget { @@ -34,6 +38,8 @@ class ContainerChips extends HookConsumerWidget { final containersAsync = ref.watch(filteredContainersWithCountProvider(searchText)); + final dragTargetTabId = useValueNotifier(null); + return containersAsync.when( data: (availableContainers) { if (selectedContainer == null && @@ -62,6 +68,74 @@ class ContainerChips extends HookConsumerWidget { itemLabel: (container) => Text(container.name ?? 'New Container'), itemBadgeCount: (container) => container.tabCount, + itemWrap: (child, container) { + return HookBuilder( + builder: (context) { + final overlayController = + useOverlayPortalController(); + + return DragTarget( + onMove: (details) { + ref.read(willAcceptDropProvider.notifier).setData( + ContainerDropData(details.data.tabId), + ); + + dragTargetTabId.value = details.data.tabId; + overlayController.show(); + }, + onLeave: (data) { + ref.read(willAcceptDropProvider.notifier).clear(); + + dragTargetTabId.value = null; + overlayController.hide(); + }, + onAcceptWithDetails: (details) async { + await ref + .read(tabDataRepositoryProvider.notifier) + .assignContainer( + details.data.tabId, + container.id, + ); + + dragTargetTabId.value = null; + overlayController.hide(); + }, + builder: (context, candidateData, rejectedData) { + final renderBox = + context.findRenderObject() as RenderBox?; + final position = + renderBox?.localToGlobal(Offset.zero) ?? + Offset.zero; + + return OverlayPortal( + controller: overlayController, + overlayChildBuilder: (context) => Positioned( + top: position.dy, + left: position.dx, + child: IgnorePointer( + child: Transform.scale( + scale: 1.1, + child: child, + ), + ), + ), + child: HookBuilder( + builder: (context) { + final dragTabId = + useValueListenable(dragTargetTabId); + + return Opacity( + opacity: (dragTabId == null) ? 1.0 : 0.0, + child: child, + ); + }, + ), + ); + }, + ); + }, + ); + }, availableItems: availableContainers, selectedItem: selectedContainer, onSelected: onSelected, diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/reorderable_grid.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/reorderable_grid.dart deleted file mode 100644 index e7b3fa86..00000000 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/reorderable_grid.dart +++ /dev/null @@ -1,1215 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - -/// {@template reorderable_grid_view.reorderable_grid} -/// A scrolling container that allows the user to interactively reorder the -/// grid items. -/// -/// This widget is similar to one created by [GridView.builder], and uses -/// an [IndexedWidgetBuilder] to create each item. -/// -/// It is up to the application to wrap each child (or an internal part of the -/// child such as a drag handle) with a drag listener that will recognize -/// the start of an item drag and then start the reorder by calling -/// [ReorderableGridState.startItemDragReorder]. This is most easily achieved -/// by wrapping each child in a [ReorderableGridDragStartListener] or a -/// [ReorderableGridDelayedDragStartListener]. These will take care of recognizing -/// the start of a drag gesture and call the grid state's -/// [ReorderableGridState.startItemDragReorder] method. -/// -/// This widget's [ReorderableGridState] can be used to manually start an item -/// reorder, or cancel a current drag. To refer to the -/// [ReorderableGridState] either provide a [GlobalKey] or use the static -/// [ReorderableGrid.of] method from an item's build method. -/// -/// See also: -/// -/// * [SliverReorderableGrid], a sliver grid that allows the user to reorder -/// its items. -/// {@endtemplate} -class ReorderableGrid extends StatefulWidget { - /// {@macro reorderable_grid_view.reorderable_grid} - /// The [itemCount] must be greater than or equal to zero. - const ReorderableGrid({ - Key? key, - required this.itemBuilder, - required this.itemCount, - required this.onReorder, - required this.gridDelegate, - this.proxyDecorator, - this.padding, - this.scrollDirection = Axis.vertical, - this.reverse = false, - this.controller, - this.primary, - this.physics, - this.shrinkWrap = false, - this.anchor = 0.0, - this.cacheExtent, - this.dragStartBehavior = DragStartBehavior.start, - this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, - this.restorationId, - this.clipBehavior = Clip.hardEdge, - this.autoScroll, - this.onReorderStart, - }) : assert(itemCount >= 0), - super(key: key); - - /// Called, as needed, to build grid item widgets. - /// - /// List items are only built when they're scrolled into view. - /// - /// The [IndexedWidgetBuilder] index parameter indicates the item's - /// position in the grid. The value of the index parameter will be between - /// zero and one less than [itemCount]. All items in the grid must have a - /// unique [Key], and should have some kind of listener to start the drag - /// (usually a [ReorderableGridDragStartListener] or - /// [ReorderableGridDelayedDragStartListener]). - final IndexedWidgetBuilder itemBuilder; - - /// {@macro flutter.widgets.reorderable_list.itemCount} - final int itemCount; - - /// {@macro flutter.widgets.reorderable_list.onReorder} - final ReorderCallback onReorder; - - /// {@macro flutter.widgets.reorderable_list.onReorderStart} - final void Function(int index)? onReorderStart; - - /// {@macro flutter.widgets.reorderable_list.proxyDecorator} - final ReorderItemProxyDecorator? proxyDecorator; - - /// {@macro flutter.widgets.reorderable_list.padding} - final EdgeInsetsGeometry? padding; - - /// {@macro flutter.widgets.scroll_view.scrollDirection} - final Axis scrollDirection; - - /// {@macro flutter.widgets.scroll_view.reverse} - final bool reverse; - - /// {@macro flutter.widgets.scroll_view.controller} - final ScrollController? controller; - - /// {@macro flutter.widgets.scroll_view.primary} - final bool? primary; - - /// {@macro flutter.widgets.scroll_view.physics} - final ScrollPhysics? physics; - - /// {@macro flutter.widgets.scroll_view.shrinkWrap} - final bool shrinkWrap; - - /// {@macro flutter.widgets.scroll_view.anchor} - final double anchor; - - /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} - final double? cacheExtent; - - /// {@macro flutter.widgets.scrollable.dragStartBehavior} - final DragStartBehavior dragStartBehavior; - - /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} - /// - /// The default is [ScrollViewKeyboardDismissBehavior.manual] - final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; - - /// {@macro flutter.widgets.scrollable.restorationId} - final String? restorationId; - - /// {@macro flutter.material.Material.clipBehavior} - /// - /// Defaults to [Clip.hardEdge]. - final Clip clipBehavior; - - final SliverGridDelegate gridDelegate; - - /// Overrides if autoscrolling is enabled. Defaults to false if `physics` is - /// [NeverScrollableScrollPhysics] - final bool? autoScroll; - - /// The state from the closest instance of this class that encloses the given - /// context. - /// - /// This method is typically used by [ReorderableGrid] item widgets that - /// insert or remove items in response to user input. - /// - /// If no [ReorderableGrid] surrounds the given context, then this function - /// will assert in debug mode and throw an exception in release mode. - /// - /// This method can be expensive (it walks the element tree). - /// - /// See also: - /// - /// * [maybeOf], a similar function that will return null if no - /// [ReorderableGrid] ancestor is found. - static ReorderableGridState of(BuildContext context) { - final ReorderableGridState? result = - context.findAncestorStateOfType(); - assert(() { - if (result == null) { - throw FlutterError.fromParts([ - ErrorSummary( - 'ReorderableGrid.of() called with a context that does not contain a ReorderableGrid.'), - ErrorDescription( - 'No ReorderableGrid ancestor could be found starting from the context that was passed to ReorderableGrid.of().', - ), - ErrorHint( - 'This can happen when the context provided is from the same StatefulWidget that ' - 'built the ReorderableGrid. Please see the ReorderableGrid documentation for examples ' - 'of how to refer to an ReorderableGridState object:\n' - ' https://api.flutter.dev/flutter/widgets/ReorderableGridState-class.html', - ), - context.describeElement('The context used was'), - ]); - } - return true; - }()); - return result!; - } - - /// The state from the closest instance of this class that encloses the given - /// context. - /// - /// This method is typically used by [ReorderableGrid] item widgets that insert - /// or remove items in response to user input. - /// - /// If no [ReorderableGrid] surrounds the context given, then this function will - /// return null. - /// - /// This method can be expensive (it walks the element tree). - /// - /// See also: - /// - /// * [of], a similar function that will throw if no [ReorderableGrid] ancestor - /// is found. - static ReorderableGridState? maybeOf(BuildContext context) { - return context.findAncestorStateOfType(); - } - - @override - ReorderableGridState createState() => ReorderableGridState(); -} - -/// The state for a grid that allows the user to interactively reorder -/// the grid items. -/// -/// An app that needs to start a new item drag or cancel an existing one -/// can refer to the [ReorderableGrid]'s state with a global key: -/// -/// ```dart -/// GlobalKey gridKey = GlobalKey(); -/// ... -/// ReorderableGrid(key: gridKey, ...); -/// ... -/// gridKey.currentState.cancelReorder(); -/// ``` -class ReorderableGridState extends State { - final GlobalKey _sliverReorderableGridKey = - GlobalKey(); - - /// Initiate the dragging of the item at [index] that was started with - /// the pointer down [event]. - /// - /// The given [recognizer] will be used to recognize and start the drag - /// item tracking and lead to either an item reorder, or a cancelled drag. - /// The grid will take ownership of the returned recognizer and will dispose - /// it when it is no longer needed. - /// - /// Most applications will not use this directly, but will wrap the item - /// (or part of the item, like a drag handle) in either a - /// [ReorderableGridDragStartListener] or [ReorderableGridDelayedDragStartListener] - /// which call this for the application. - void startItemDragReorder({ - required int index, - required PointerDownEvent event, - required MultiDragGestureRecognizer recognizer, - }) { - _sliverReorderableGridKey.currentState!.startItemDragReorder( - index: index, event: event, recognizer: recognizer); - } - - /// Cancel any item drag in progress. - /// - /// This should be called before any major changes to the item grid - /// occur so that any item drags will not get confused by - /// changes to the underlying grid. - /// - /// If no drag is active, this will do nothing. - void cancelReorder() { - _sliverReorderableGridKey.currentState!.cancelReorder(); - } - - @override - Widget build(BuildContext context) { - return CustomScrollView( - scrollDirection: widget.scrollDirection, - reverse: widget.reverse, - controller: widget.controller, - primary: widget.primary, - physics: widget.physics, - shrinkWrap: widget.shrinkWrap, - anchor: widget.anchor, - cacheExtent: widget.cacheExtent, - dragStartBehavior: widget.dragStartBehavior, - keyboardDismissBehavior: widget.keyboardDismissBehavior, - restorationId: widget.restorationId, - clipBehavior: widget.clipBehavior, - slivers: [ - SliverPadding( - padding: widget.padding ?? EdgeInsets.zero, - sliver: SliverReorderableGrid( - key: _sliverReorderableGridKey, - gridDelegate: widget.gridDelegate, - itemBuilder: widget.itemBuilder, - itemCount: widget.itemCount, - onReorder: widget.onReorder, - onReorderStart: widget.onReorderStart, - proxyDecorator: widget.proxyDecorator, - reverse: widget.reverse, - autoScroll: widget.autoScroll ?? - widget.physics is! NeverScrollableScrollPhysics, - scrollDirection: widget.scrollDirection, - ), - ), - ], - ); - } -} - -/// A sliver grid that allows the user to interactively reorder the grid items. -/// -/// It is up to the application to wrap each child (or an internal part of the -/// child) with a drag listener that will recognize the start of an item drag -/// and then start the reorder by calling -/// [SliverReorderableGridState.startItemDragReorder]. This is most easily -/// achieved by wrapping each child in a [ReorderableGridDragStartListener] or -/// a [ReorderableGridDelayedDragStartListener]. These will take care of -/// recognizing the start of a drag gesture and call the grid state's start -/// item drag method. -/// -/// This widget's [SliverReorderableGridState] can be used to manually start an item -/// reorder, or cancel a current drag that's already underway. To refer to the -/// [SliverReorderableGridState] either provide a [GlobalKey] or use the static -/// [SliverReorderableGrid.of] method from an item's build method. -/// -/// See also: -/// -/// * [ReorderableGrid], a regular widget grid that allows the user to reorder -/// its items. -class SliverReorderableGrid extends StatefulWidget { - /// Creates a sliver grid that allows the user to interactively reorder its - /// items. - /// - /// The [itemCount] must be greater than or equal to zero. - const SliverReorderableGrid({ - Key? key, - required this.itemBuilder, - required this.itemCount, - required this.onReorder, - required this.gridDelegate, - this.onReorderStart, - this.reverse = false, - this.proxyDecorator, - this.autoScroll = true, - this.scrollDirection = Axis.vertical, - }) : assert(itemCount >= 0), - super(key: key); - - /// {@macro flutter.widgets.reorderable_list.itemBuilder} - final IndexedWidgetBuilder itemBuilder; - - /// {@macro flutter.widgets.reorderable_list.itemCount} - final int itemCount; - - /// {@macro flutter.widgets.reorderable_list.onReorder} - final ReorderCallback onReorder; - - /// {@macro flutter.widgets.reorderable_list.onReorderStart} - final void Function(int index)? onReorderStart; - - /// {@macro flutter.widgets.reorderable_list.proxyDecorator} - final ReorderItemProxyDecorator? proxyDecorator; - - final SliverGridDelegate gridDelegate; - - /// If auto scrolling is enabled. Should be disabled if associated scroll - /// physics are [NeverScrollableScrollPhysics] - final bool autoScroll; - - /// {@macro flutter.widgets.scroll_view.reverse} - final bool reverse; - - /// {@macro flutter.widgets.scroll_view.scrollDirection} - final Axis scrollDirection; - - @override - SliverReorderableGridState createState() => SliverReorderableGridState(); - - /// The state from the closest instance of this class that encloses the given - /// context. - /// - /// This method is typically used by [SliverReorderableGrid] item widgets to - /// start or cancel an item drag operation. - /// - /// If no [SliverReorderableGrid] surrounds the context given, this function - /// will assert in debug mode and throw an exception in release mode. - /// - /// This method can be expensive (it walks the element tree). - /// - /// See also: - /// - /// * [maybeOf], a similar function that will return null if no - /// [SliverReorderableGrid] ancestor is found. - static SliverReorderableGridState of(BuildContext context) { - final SliverReorderableGridState? result = - context.findAncestorStateOfType(); - assert(() { - if (result == null) { - throw FlutterError.fromParts([ - ErrorSummary( - 'SliverReorderableGrid.of() called with a context that does not contain a SliverReorderableGrid.', - ), - ErrorDescription( - 'No SliverReorderableGrid ancestor could be found starting from the context that was passed to SliverReorderableGrid.of().', - ), - ErrorHint( - 'This can happen when the context provided is from the same StatefulWidget that ' - 'built the SliverReorderableGrid. Please see the SliverReorderableGrid documentation for examples'), - context.describeElement('The context used was'), - ]); - } - return true; - }()); - return result!; - } - - /// The state from the closest instance of this class that encloses the given - /// context. - /// - /// This method is typically used by [SliverReorderableGrid] item widgets that - /// insert or remove items in response to user input. - /// - /// If no [SliverReorderableGrid] surrounds the context given, this function - /// will return null. - /// - /// This method can be expensive (it walks the element tree). - /// - /// See also: - /// - /// * [of], a similar function that will throw if no [SliverReorderableGrid] - /// ancestor is found. - static SliverReorderableGridState? maybeOf(BuildContext context) { - return context.findAncestorStateOfType(); - } -} - -/// The state for a sliver grid that allows the user to interactively reorder -/// the grid items. -/// -/// An app that needs to start a new item drag or cancel an existing one -/// can refer to the [SliverReorderableGrid]'s state with a global key: -/// -/// ```dart -/// GlobalKey gridKey = GlobalKey(); -/// ... -/// SliverReorderableGrid(key: gridKey, ...); -/// ... -/// gridKey.currentState.cancelReorder(); -/// ``` -/// -/// [ReorderableGridDragStartListener] and [ReorderableGridDelayedDragStartListener] -/// refer to their [SliverReorderableGrid] with the static -/// [SliverReorderableGrid.of] method. -class SliverReorderableGridState extends State - with TickerProviderStateMixin { - // Map of index -> child state used manage where the dragging item will need - // to be inserted. - final Map _items = {}; - - OverlayEntry? _overlayEntry; - int? _dragIndex; - _DragInfo? _dragInfo; - int? _insertIndex; - Offset? _finalDropPosition; - MultiDragGestureRecognizer? _recognizer; - bool _autoScrolling = false; - - @override - void didUpdateWidget(covariant SliverReorderableGrid oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.itemCount != oldWidget.itemCount) { - cancelReorder(); - } - } - - @override - void dispose() { - _dragInfo?.dispose(); - super.dispose(); - } - - /// Initiate the dragging of the item at [index] that was started with - /// the pointer down [event]. - /// - /// The given [recognizer] will be used to recognize and start the drag - /// item tracking and lead to either an item reorder, or a cancelled drag. - /// - /// Most applications will not use this directly, but will wrap the item - /// (or part of the item, like a drag handle) in either a - /// [ReorderableGridDragStartListener] or [ReorderableGridDelayedDragStartListener] - /// which call this method when they detect the gesture that triggers a drag - /// start. - void startItemDragReorder({ - required int index, - required PointerDownEvent event, - required MultiDragGestureRecognizer recognizer, - }) { - assert(0 <= index && index < widget.itemCount); - setState(() { - if (_dragInfo != null) { - cancelReorder(); - } - if (_items.containsKey(index)) { - _dragIndex = index; - _recognizer = recognizer - ..onStart = _dragStart - ..addPointer(event); - } else { - throw Exception('Attempting to start a drag on a non-visible item'); - } - }); - } - - /// Cancel any item drag in progress. - /// - /// This should be called before any major changes to the item grid - /// occur so that any item drags will not get confused by - /// changes to the underlying grid. - /// - /// If a drag operation is in progress, this will immediately reset - /// the grid to back to its pre-drag state. - /// - /// If no drag is active, this will do nothing. - void cancelReorder() { - _dragReset(); - } - - void _registerItem(_ReorderableItemState item) { - _items[item.index] = item; - if (item.index == _dragInfo?.index) { - item.dragging = true; - item.rebuild(); - } - } - - void _unregisterItem(int index, _ReorderableItemState item) { - final _ReorderableItemState? currentItem = _items[index]; - if (currentItem == item) { - _items.remove(index); - } - } - - Drag? _dragStart(Offset position) { - assert(_dragInfo == null); - - // if any items are already being dragged, ignore this dragstart - if (_items.values.any((e) => e.dragging)) return null; - - final _ReorderableItemState item = _items[_dragIndex!]!; - item.dragging = true; - item.rebuild(); - widget.onReorderStart?.call(_dragIndex!); - - _insertIndex = item.index; - - _dragInfo = _DragInfo( - item: item, - initialPosition: position, - onUpdate: _dragUpdate, - onCancel: _dragCancel, - onEnd: _dragEnd, - onDropCompleted: _dropCompleted, - proxyDecorator: widget.proxyDecorator, - tickerProvider: this, - ); - _dragInfo!.startDrag(); - - final OverlayState overlay = Overlay.of(context); - assert(_overlayEntry == null); - _overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy); - overlay.insert(_overlayEntry!); - - for (final _ReorderableItemState childItem in _items.values) { - if (childItem == item || !childItem.mounted) continue; - childItem.updateForGap(_insertIndex!, false); - } - return _dragInfo; - } - - void _dragUpdate(_DragInfo item, Offset position, Offset delta) { - setState(() { - _overlayEntry?.markNeedsBuild(); - _dragUpdateItems(); - _autoScrollIfNecessary(); - }); - } - - void _dragCancel(_DragInfo item) { - _dragReset(); - } - - void _dragEnd(_DragInfo item) { - setState(() => _finalDropPosition = _itemOffsetAt(_insertIndex!)); - } - - void _dropCompleted() { - final int fromIndex = _dragIndex!; - final int toIndex = _insertIndex!; - if (fromIndex != toIndex) { - widget.onReorder.call(fromIndex, toIndex); - } - _dragReset(); - } - - void _dragReset() { - setState(() { - if (_dragInfo != null) { - if (_dragIndex != null && _items.containsKey(_dragIndex)) { - final _ReorderableItemState dragItem = _items[_dragIndex!]!; - dragItem._dragging = false; - dragItem.rebuild(); - _dragIndex = null; - } - _dragInfo?.dispose(); - _dragInfo = null; - _resetItemGap(); - _recognizer?.dispose(); - _recognizer = null; - _overlayEntry?.remove(); - _overlayEntry = null; - _finalDropPosition = null; - } - }); - } - - void _resetItemGap() { - for (final _ReorderableItemState item in _items.values) { - item.resetGap(); - } - } - - void _dragUpdateItems() { - assert(_dragInfo != null); - - int newIndex = _insertIndex!; - - final dragCenter = _dragInfo!.itemSize - .center(_dragInfo!.dragPosition - _dragInfo!.dragOffset); - - for (final _ReorderableItemState item in _items.values) { - if (!item.mounted) continue; - - final Rect geometry = item.targetGeometryNonOffset(); - - if (geometry.contains(dragCenter)) { - newIndex = item.index; - break; - } - } - - if (newIndex == _insertIndex) return; - _insertIndex = newIndex; - - for (final _ReorderableItemState item in _items.values) { - item.updateForGap(_insertIndex!, true); - } - } - - Future _autoScrollIfNecessary() async { - if (_autoScrolling || - _dragInfo == null || - _dragInfo!.scrollable == null || - widget.autoScroll == false) { - return; - } - - final position = _dragInfo!.scrollable!.position; - double? newOffset; - - const duration = Duration(milliseconds: 14); - const step = 1.0; - const overDragMax = 20.0; - const overDragCoef = 10; - - final isVertical = widget.scrollDirection == Axis.vertical; - final isReversed = widget.reverse; - - /// get the scroll window position on the screen - final scrollRenderBox = - _dragInfo!.scrollable!.context.findRenderObject()! as RenderBox; - final Offset scrollPosition = scrollRenderBox.localToGlobal(Offset.zero); - - /// calculate the start and end position for the scroll window - double scrollWindowStart = - isVertical ? scrollPosition.dy : scrollPosition.dx; - double scrollWindowEnd = scrollWindowStart + - (isVertical ? scrollRenderBox.size.height : scrollRenderBox.size.width); - - /// get the proxy (dragged) object's position on the screen - final proxyObjectPosition = _dragInfo!.dragPosition - _dragInfo!.dragOffset; - - /// calculate the start and end position for the proxy object - double proxyObjectStart = - isVertical ? proxyObjectPosition.dy : proxyObjectPosition.dx; - double proxyObjectEnd = proxyObjectStart + - (isVertical ? _dragInfo!.itemSize.height : _dragInfo!.itemSize.width); - - if (!isReversed) { - /// if start of proxy object is before scroll window - if (proxyObjectStart < scrollWindowStart && - position.pixels > position.minScrollExtent) { - final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax); - newOffset = max(position.minScrollExtent, - position.pixels - step * overDrag / overDragCoef); - } - - /// if end of proxy object is after scroll window - else if (proxyObjectEnd > scrollWindowEnd && - position.pixels < position.maxScrollExtent) { - final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax); - newOffset = min(position.maxScrollExtent, - position.pixels + step * overDrag / overDragCoef); - } - } else { - /// if start of proxy object is before scroll window - if (proxyObjectStart < scrollWindowStart && - position.pixels < position.maxScrollExtent) { - final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax); - newOffset = max(position.minScrollExtent, - position.pixels + step * overDrag / overDragCoef); - } - - /// if end of proxy object is after scroll window - else if (proxyObjectEnd > scrollWindowEnd && - position.pixels > position.minScrollExtent) { - final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax); - newOffset = min(position.maxScrollExtent, - position.pixels - step * overDrag / overDragCoef); - } - } - - if (newOffset != null && (newOffset - position.pixels).abs() >= 1.0) { - _autoScrolling = true; - await position.animateTo( - newOffset, - duration: duration, - curve: Curves.linear, - ); - _autoScrolling = false; - if (_dragInfo != null) { - _dragUpdateItems(); - _autoScrollIfNecessary(); - } - } - } - - Offset _calculateNextDragOffset(int index) { - int minPos = min(_dragIndex!, _insertIndex!); - int maxPos = max(_dragIndex!, _insertIndex!); - - if (index < minPos || index > maxPos) return Offset.zero; - - final int direction = _insertIndex! > _dragIndex! ? -1 : 1; - return _itemOffsetAt(index + direction) - _itemOffsetAt(index); - } - - Offset _itemOffsetAt(int index) { - final box = _items[index]?.context.findRenderObject() as RenderBox?; - if (box == null) return Offset.zero; - - return box.localToGlobal(Offset.zero); - } - - Widget _itemBuilder(BuildContext context, int index) { - if (_dragInfo != null && index >= widget.itemCount) { - return SizedBox.fromSize(size: _dragInfo!.itemSize); - } - - final Widget child = widget.itemBuilder(context, index); - assert(child.key != null, 'All grid items must have a key'); - - final OverlayState overlay = Overlay.of(context); - return _ReorderableItem( - key: _ReorderableItemGlobalKey(child.key!, index, this), - index: index, - capturedThemes: - InheritedTheme.capture(from: context, to: overlay.context), - child: child, - ); - } - - @override - Widget build(BuildContext context) { - assert(debugCheckHasOverlay(context)); - final SliverChildBuilderDelegate childrenDelegate = - SliverChildBuilderDelegate( - _itemBuilder, - childCount: widget.itemCount, - ); - return SliverGrid( - delegate: childrenDelegate, - gridDelegate: widget.gridDelegate, - ); - } -} - -class _ReorderableItem extends StatefulWidget { - const _ReorderableItem({ - required Key key, - required this.index, - required this.child, - required this.capturedThemes, - }) : super(key: key); - - final int index; - final Widget child; - final CapturedThemes capturedThemes; - - @override - _ReorderableItemState createState() => _ReorderableItemState(); -} - -class _ReorderableItemState extends State<_ReorderableItem> { - late SliverReorderableGridState _listState; - - Offset _startOffset = Offset.zero; - Offset _targetOffset = Offset.zero; - AnimationController? _offsetAnimation; - - Key get key => widget.key!; - int get index => widget.index; - - bool get dragging => _dragging; - set dragging(bool dragging) { - if (mounted) { - setState(() { - _dragging = dragging; - }); - } - } - - bool _dragging = false; - - @override - void initState() { - _listState = SliverReorderableGrid.of(context); - _listState._registerItem(this); - super.initState(); - } - - @override - void dispose() { - _offsetAnimation?.dispose(); - _listState._unregisterItem(index, this); - super.dispose(); - } - - @override - void didUpdateWidget(covariant _ReorderableItem oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.index != widget.index) { - _listState._unregisterItem(oldWidget.index, this); - _listState._registerItem(this); - } - } - - @override - Widget build(BuildContext context) { - if (_dragging) { - return const SizedBox.shrink(); - } - _listState._registerItem(this); - return Transform( - transform: Matrix4.translationValues(offset.dx, offset.dy, 0.0), - child: widget.child, - ); - } - - @override - void deactivate() { - _listState._unregisterItem(index, this); - super.deactivate(); - } - - Offset get offset { - if (_offsetAnimation != null) { - final double animValue = - Curves.easeInOut.transform(_offsetAnimation!.value); - return Offset.lerp(_startOffset, _targetOffset, animValue)!; - } - return _targetOffset; - } - - void updateForGap(int gapIndex, bool animate) { - if (!mounted) return; - - final Offset newTargetOffset = _listState._calculateNextDragOffset(index); - - if (newTargetOffset == _targetOffset) return; - _targetOffset = newTargetOffset; - - if (animate) { - if (_offsetAnimation == null) { - _offsetAnimation = AnimationController( - vsync: _listState, - duration: const Duration(milliseconds: 250), - ) - ..addListener(rebuild) - ..addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.completed) { - _startOffset = _targetOffset; - _offsetAnimation!.dispose(); - _offsetAnimation = null; - } - }) - ..forward(); - } else { - _startOffset = offset; - _offsetAnimation!.forward(from: 0.0); - } - } else { - if (_offsetAnimation != null) { - _offsetAnimation!.dispose(); - _offsetAnimation = null; - } - _startOffset = _targetOffset; - } - rebuild(); - } - - void resetGap() { - if (_offsetAnimation != null) { - _offsetAnimation!.dispose(); - _offsetAnimation = null; - } - _startOffset = Offset.zero; - _targetOffset = Offset.zero; - rebuild(); - } - - Rect targetGeometry() { - final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox; - final Offset itemPosition = - itemRenderBox.localToGlobal(Offset.zero) + _targetOffset; - return itemPosition & itemRenderBox.size; - } - - Rect targetGeometryNonOffset() { - final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox; - final Offset itemPosition = itemRenderBox.localToGlobal(Offset.zero); - return itemPosition & itemRenderBox.size; - } - - void rebuild() { - if (mounted) { - setState(() {}); - } - } -} - -/// A wrapper widget that will recognize the start of a drag on the wrapped -/// widget by a [PointerDownEvent], and immediately initiate dragging the -/// wrapped item to a new location in a reorderable grid. -/// -/// See also: -/// -/// * [ReorderableGridDelayedDragStartListener], a similar wrapper that will -/// only recognize the start after a long press event. -/// * [ReorderableGrid], a widget grid that allows the user to reorder -/// its items. -/// * [SliverReorderableGrid], a sliver grid that allows the user to reorder -/// its items. -/// * [ReorderableGridView], a material design grid that allows the user to -/// reorder its items. -class ReorderableGridDragStartListener extends StatelessWidget { - /// Creates a listener for a drag immediately following a pointer down - /// event over the given child widget. - /// - /// This is most commonly used to wrap part of a grid item like a drag - /// handle. - const ReorderableGridDragStartListener({ - Key? key, - required this.child, - required this.index, - this.enabled = true, - }) : super(key: key); - - /// The widget for which the application would like to respond to a tap and - /// drag gesture by starting a reordering drag on a reorderable grid. - final Widget child; - - /// The index of the associated item that will be dragged in the grid. - final int index; - - /// Whether the [child] item can be dragged and moved in the grid. - /// - /// If true, the item can be moved to another location in the grid when the - /// user taps on the child. If false, tapping on the child will be ignored. - final bool enabled; - - @override - Widget build(BuildContext context) { - return Listener( - onPointerDown: enabled - ? (PointerDownEvent event) => _startDragging(context, event) - : null, - child: child, - ); - } - - /// Provides the gesture recognizer used to indicate the start of a reordering - /// drag operation. - /// - /// By default this returns an [ImmediateMultiDragGestureRecognizer] but - /// subclasses can use this to customize the drag start gesture. - @protected - MultiDragGestureRecognizer createRecognizer() { - return ImmediateMultiDragGestureRecognizer(debugOwner: this); - } - - void _startDragging(BuildContext context, PointerDownEvent event) { - final SliverReorderableGridState? list = - SliverReorderableGrid.maybeOf(context); - list?.startItemDragReorder( - index: index, - event: event, - recognizer: createRecognizer(), - ); - } -} - -/// A wrapper widget that will recognize the start of a drag operation by -/// looking for a long press event. Once it is recognized, it will start -/// a drag operation on the wrapped item in the reorderable grid. -/// -/// See also: -/// -/// * [ReorderableGridDragStartListener], a similar wrapper that will -/// recognize the start of the drag immediately after a pointer down event. -/// * [ReorderableGrid], a widget grid that allows the user to reorder -/// its items. -/// * [SliverReorderableGrid], a sliver grid that allows the user to reorder -/// its items. -/// * [ReorderableGridView], a material design grid that allows the user to -/// reorder its items. -class ReorderableGridDelayedDragStartListener - extends ReorderableGridDragStartListener { - /// Creates a listener for an drag following a long press event over the - /// given child widget. - /// - /// This is most commonly used to wrap an entire grid item in a reorderable - /// grid. - const ReorderableGridDelayedDragStartListener({ - Key? key, - required Widget child, - required int index, - bool enabled = true, - }) : super(key: key, child: child, index: index, enabled: enabled); - - @override - MultiDragGestureRecognizer createRecognizer() { - return DelayedMultiDragGestureRecognizer(debugOwner: this); - } -} - -typedef _DragItemUpdate = void Function( - _DragInfo item, Offset position, Offset delta); -typedef _DragItemCallback = void Function(_DragInfo item); - -class _DragInfo extends Drag { - _DragInfo({ - required _ReorderableItemState item, - Offset initialPosition = Offset.zero, - this.onUpdate, - this.onEnd, - this.onCancel, - this.onDropCompleted, - this.proxyDecorator, - required this.tickerProvider, - }) { - final RenderBox itemRenderBox = - item.context.findRenderObject()! as RenderBox; - listState = item._listState; - index = item.index; - child = item.widget.child; - capturedThemes = item.widget.capturedThemes; - dragPosition = initialPosition; - dragOffset = itemRenderBox.globalToLocal(initialPosition); - itemSize = item.context.size!; - scrollable = Scrollable.of(item.context); - } - - final _DragItemUpdate? onUpdate; - final _DragItemCallback? onEnd; - final _DragItemCallback? onCancel; - final VoidCallback? onDropCompleted; - final ReorderItemProxyDecorator? proxyDecorator; - final TickerProvider tickerProvider; - - late SliverReorderableGridState listState; - late int index; - late Widget child; - late Offset dragPosition; - late Offset dragOffset; - late Size itemSize; - late CapturedThemes capturedThemes; - ScrollableState? scrollable; - AnimationController? _proxyAnimation; - - void dispose() { - _proxyAnimation?.dispose(); - } - - void startDrag() { - _proxyAnimation = AnimationController( - vsync: tickerProvider, - duration: const Duration(milliseconds: 250), - ) - ..addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.dismissed) { - _dropCompleted(); - } - }) - ..forward(); - } - - @override - void update(DragUpdateDetails details) { - dragPosition += details.delta; - onUpdate?.call(this, dragPosition, details.delta); - } - - @override - void end(DragEndDetails details) { - _proxyAnimation!.reverse(); - onEnd?.call(this); - } - - @override - void cancel() { - _proxyAnimation?.dispose(); - _proxyAnimation = null; - onCancel?.call(this); - } - - void _dropCompleted() { - _proxyAnimation?.dispose(); - _proxyAnimation = null; - onDropCompleted?.call(); - } - - Widget createProxy(BuildContext context) { - return capturedThemes.wrap( - _DragItemProxy( - listState: listState, - index: index, - size: itemSize, - animation: _proxyAnimation!, - position: dragPosition - dragOffset - _overlayOrigin(context), - proxyDecorator: proxyDecorator, - child: child, - ), - ); - } -} - -Offset _overlayOrigin(BuildContext context) { - final OverlayState overlay = Overlay.of(context); - final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox; - return overlayBox.localToGlobal(Offset.zero); -} - -class _DragItemProxy extends StatelessWidget { - const _DragItemProxy({ - Key? key, - required this.listState, - required this.index, - required this.child, - required this.position, - required this.size, - required this.animation, - required this.proxyDecorator, - }) : super(key: key); - - final SliverReorderableGridState listState; - final int index; - final Widget child; - final Offset position; - final Size size; - final AnimationController animation; - final ReorderItemProxyDecorator? proxyDecorator; - - @override - Widget build(BuildContext context) { - final Widget proxyChild = - proxyDecorator?.call(child, index, animation.view) ?? child; - final Offset overlayOrigin = _overlayOrigin(context); - - return MediaQuery( - // Remove the top padding so that any nested grid views in the item - // won't pick up the scaffold's padding in the overlay. - data: MediaQuery.of(context).removePadding(removeTop: true), - child: AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget? child) { - Offset effectivePosition = position; - final Offset? dropPosition = listState._finalDropPosition; - if (dropPosition != null) { - effectivePosition = Offset.lerp(dropPosition - overlayOrigin, - effectivePosition, Curves.easeOut.transform(animation.value))!; - } - return Positioned( - left: effectivePosition.dx, - top: effectivePosition.dy, - child: SizedBox( - width: size.width, - height: size.height, - child: child, - ), - ); - }, - child: proxyChild, - ), - ); - } -} - -// A global key that takes its identity from the object and uses a value of a -// particular type to identify itself. -// -// The difference with GlobalObjectKey is that it uses [==] instead of [identical] -// of the objects used to generate widgets. -@optionalTypeArgs -class _ReorderableItemGlobalKey extends GlobalObjectKey { - const _ReorderableItemGlobalKey(this.subKey, this.index, this.state) - : super(subKey); - - final Key subKey; - final int index; - final SliverReorderableGridState state; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) return false; - return other is _ReorderableItemGlobalKey && - other.subKey == subKey && - other.index == index && - other.state == state; - } - - @override - int get hashCode => Object.hash(subKey, index, state); -} diff --git a/app/lib/features/search/presentation/screens/search.dart b/app/lib/features/search/presentation/screens/search.dart index 91569f24..4a1747a8 100644 --- a/app/lib/features/search/presentation/screens/search.dart +++ b/app/lib/features/search/presentation/screens/search.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; diff --git a/app/lib/features/settings/presentation/screens/browser_hardening.dart b/app/lib/features/settings/presentation/screens/browser_hardening.dart new file mode 100644 index 00000000..870c5681 --- /dev/null +++ b/app/lib/features/settings/presentation/screens/browser_hardening.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/core/routing/routes.dart'; +import 'package:lensai/data/models/equatable_iterable.dart'; +import 'package:lensai/features/geckoview/features/preferences/data/repositories/preference_settings.dart'; +import 'package:lensai/features/settings/presentation/widgets/hardening_group_icon.dart'; +import 'package:lensai/presentation/widgets/failure_widget.dart'; + +class BrowserHardeningScreen extends HookConsumerWidget { + const BrowserHardeningScreen(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final preferenceGroups = + ref.watch(preferenceSettingsGeneralRepositoryProvider); + + final allGroupsActive = useMemoized( + () => + preferenceGroups.valueOrNull?.values.every( + (element) => element.isActive, + ) ?? + false, + [EquatableCollection(preferenceGroups.valueOrNull, immutable: true)], + ); + + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar(title: const Text('Browser Hardening')), + body: preferenceGroups.when( + data: (data) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + color: theme.colorScheme.primaryContainer, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SwitchListTile( + value: allGroupsActive, + title: Text( + 'Complete Hardening', + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + ), + ), + onChanged: (value) async { + final notifier = ref.read( + preferenceSettingsGeneralRepositoryProvider.notifier, + ); + + if (value) { + await notifier.apply(); + } else { + await notifier.reset(); + } + }, + ), + ), + ), + ), + Expanded( + child: ListView( + children: data.entries.map( + (group) { + return Row( + children: [ + Expanded( + child: ListTile( + title: Text(group.key), + subtitle: (group.value.description != null) + ? Text(group.value.description!) + : null, + leading: HardeningGroupIcon( + isActive: group.value.isActive, + isPartlyActive: group.value.isPartlyActive, + ), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await context.push( + BrowserHardeningGroupRoute( + group: group.key, + ).location, + ); + }, + ), + ), + ], + ); + }, + ).toList(), + ), + ), + ], + ); + }, + error: (error, stackTrace) => FailureWidget( + title: 'Could not load preference settings', + exception: error, + onRetry: () => + ref.refresh(preferenceSettingsGeneralRepositoryProvider), + ), + loading: () => const SizedBox.shrink(), + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/browser_hardening_group.dart b/app/lib/features/settings/presentation/screens/browser_hardening_group.dart new file mode 100644 index 00000000..3a0cf81f --- /dev/null +++ b/app/lib/features/settings/presentation/screens/browser_hardening_group.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/geckoview/features/preferences/data/repositories/preference_settings.dart'; +import 'package:lensai/features/settings/presentation/widgets/hardening_group_icon.dart'; +import 'package:lensai/presentation/widgets/failure_widget.dart'; + +class BrowserHardeningGroupScreen extends HookConsumerWidget { + final String groupName; + + const BrowserHardeningGroupScreen({super.key, required this.groupName}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = + ref.watch(preferenceSettingsGroupRepositoryProvider(groupName)); + + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar(title: Text(groupName)), + body: settings.when( + data: (group) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + color: theme.colorScheme.primaryContainer, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SwitchListTile( + value: group.isActive, + title: Text( + groupName, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + ), + ), + subtitle: (group.description != null) + ? Text( + group.description!, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + ), + ) + : null, + onChanged: (value) async { + final notifier = ref.read( + preferenceSettingsGroupRepositoryProvider(groupName) + .notifier, + ); + + if (value) { + await notifier.apply(); + } else { + await notifier.reset(); + } + }, + ), + ), + ), + ), + Expanded( + child: ListView( + children: group.settings.entries.map( + (setting) { + return Row( + children: [ + if (!setting.value.shouldBeDefault || + !setting.value.isActive) + Expanded( + child: SwitchListTile( + value: setting.value.isActive, + title: Text(setting.value.title), + subtitle: (setting.value.description != null) + ? Text(setting.value.description!) + : null, + secondary: HardeningGroupIcon( + isActive: setting.value.isActive, + ), + onChanged: (value) async { + final notifier = ref.read( + preferenceSettingsGroupRepositoryProvider( + groupName, + ).notifier, + ); + + if (value) { + await notifier.apply(filter: [setting.key]); + } else { + await notifier.reset(filter: [setting.key]); + } + }, + ), + ) + else + Expanded( + child: ListTile( + title: Text(setting.value.title), + subtitle: (setting.value.description != null) + ? Text(setting.value.description!) + : null, + leading: HardeningGroupIcon( + isActive: setting.value.isActive, + ), + trailing: Padding( + padding: const EdgeInsets.only(right: 18.0), + child: const Icon(Icons.check), + ), + ), + ), + ], + ); + }, + ).toList(), + ), + ), + ], + ); + }, + error: (error, stackTrace) => FailureWidget( + title: 'Could not load preference settings', + exception: error, + onRetry: () => + ref.refresh(preferenceSettingsGroupRepositoryProvider(groupName)), + ), + loading: () => const SizedBox.shrink(), + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/settings.dart b/app/lib/features/settings/presentation/screens/settings.dart index 726d1f65..084a19eb 100644 --- a/app/lib/features/settings/presentation/screens/settings.dart +++ b/app/lib/features/settings/presentation/screens/settings.dart @@ -1,7 +1,9 @@ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/bangs/data/models/bang.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/settings/presentation/controllers/save_settings.dart'; @@ -50,6 +52,17 @@ class SettingsScreen extends HookConsumerWidget { controller: controller, children: [ _buildSection(theme, 'General'), + Card( + child: ListTile( + title: const Text('Browser Hardening'), + trailing: IconButton( + onPressed: () async { + await context.push(BrowserHardeningRoute().location); + }, + icon: const Icon(Icons.chevron_right), + ), + ), + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), diff --git a/app/lib/features/settings/presentation/widgets/hardening_group_icon.dart b/app/lib/features/settings/presentation/widgets/hardening_group_icon.dart new file mode 100644 index 00000000..fd3d3931 --- /dev/null +++ b/app/lib/features/settings/presentation/widgets/hardening_group_icon.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; + +class HardeningGroupIcon extends StatelessWidget { + final bool isActive; + final bool isPartlyActive; + + const HardeningGroupIcon({ + super.key, + required this.isActive, + this.isPartlyActive = false, + }); + + @override + Widget build(BuildContext context) { + if (isActive) { + return Icon( + MdiIcons.shieldLockOutline, + color: Theme.of(context).colorScheme.secondary, + ); + } else if (isPartlyActive) { + return Icon( + MdiIcons.shieldLockOpenOutline, + color: Theme.of(context).colorScheme.secondary, + ); + } + + return Icon( + MdiIcons.shieldOffOutline, + color: Theme.of(context).colorScheme.error, + ); + } +} diff --git a/app/lib/features/user/data/models/engine_settings.dart b/app/lib/features/user/data/models/engine_settings.dart new file mode 100644 index 00000000..fbd4fcbd --- /dev/null +++ b/app/lib/features/user/data/models/engine_settings.dart @@ -0,0 +1,53 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'engine_settings.g.dart'; + +@CopyWith() +@JsonSerializable(includeIfNull: true, constructor: 'withDefaults') +class EngineSettings extends GeckoEngineSettings { + EngineSettings({ + super.javascriptEnabled, + super.trackingProtectionPolicy, + super.httpsOnlyMode, + super.globalPrivacyControlEnabled, + super.preferredColorScheme, + super.cookieBannerHandlingMode, + super.cookieBannerHandlingModePrivateBrowsing, + super.cookieBannerHandlingGlobalRules, + super.cookieBannerHandlingGlobalRulesSubFrames, + super.webContentIsolationStrategy, + }); + + EngineSettings.withDefaults({ + bool? javascriptEnabled, + TrackingProtectionPolicy? trackingProtectionPolicy, + HttpsOnlyMode? httpsOnlyMode, + bool? globalPrivacyControlEnabled, + ColorScheme? preferredColorScheme, + CookieBannerHandlingMode? cookieBannerHandlingMode, + CookieBannerHandlingMode? cookieBannerHandlingModePrivateBrowsing, + bool? cookieBannerHandlingGlobalRules, + bool? cookieBannerHandlingGlobalRulesSubFrames, + WebContentIsolationStrategy? webContentIsolationStrategy, + }) : super( + javascriptEnabled: javascriptEnabled ?? true, + trackingProtectionPolicy: + trackingProtectionPolicy ?? TrackingProtectionPolicy.strict, + httpsOnlyMode: httpsOnlyMode ?? HttpsOnlyMode.enabled, + globalPrivacyControlEnabled: globalPrivacyControlEnabled ?? true, + preferredColorScheme: preferredColorScheme ?? ColorScheme.system, + cookieBannerHandlingMode: + cookieBannerHandlingMode ?? CookieBannerHandlingMode.rejectAll, + cookieBannerHandlingModePrivateBrowsing: + cookieBannerHandlingModePrivateBrowsing ?? + CookieBannerHandlingMode.rejectAll, + cookieBannerHandlingGlobalRules: + cookieBannerHandlingGlobalRules ?? true, + cookieBannerHandlingGlobalRulesSubFrames: + cookieBannerHandlingGlobalRulesSubFrames ?? true, + webContentIsolationStrategy: webContentIsolationStrategy ?? + WebContentIsolationStrategy.isolateHighValue, + ); +} diff --git a/app/lib/features/user/data/models/engine_settings.g.dart b/app/lib/features/user/data/models/engine_settings.g.dart new file mode 100644 index 00000000..81e9305f --- /dev/null +++ b/app/lib/features/user/data/models/engine_settings.g.dart @@ -0,0 +1,275 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'engine_settings.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$EngineSettingsCWProxy { + EngineSettings javascriptEnabled(bool? javascriptEnabled); + + EngineSettings trackingProtectionPolicy( + TrackingProtectionPolicy? trackingProtectionPolicy); + + EngineSettings httpsOnlyMode(HttpsOnlyMode? httpsOnlyMode); + + EngineSettings globalPrivacyControlEnabled(bool? globalPrivacyControlEnabled); + + EngineSettings preferredColorScheme(ColorScheme? preferredColorScheme); + + EngineSettings cookieBannerHandlingMode( + CookieBannerHandlingMode? cookieBannerHandlingMode); + + EngineSettings cookieBannerHandlingModePrivateBrowsing( + CookieBannerHandlingMode? cookieBannerHandlingModePrivateBrowsing); + + EngineSettings cookieBannerHandlingGlobalRules( + bool? cookieBannerHandlingGlobalRules); + + EngineSettings cookieBannerHandlingGlobalRulesSubFrames( + bool? cookieBannerHandlingGlobalRulesSubFrames); + + EngineSettings webContentIsolationStrategy( + WebContentIsolationStrategy? webContentIsolationStrategy); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `EngineSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// EngineSettings(...).copyWith(id: 12, name: "My name") + /// ```` + EngineSettings call({ + bool? javascriptEnabled, + TrackingProtectionPolicy? trackingProtectionPolicy, + HttpsOnlyMode? httpsOnlyMode, + bool? globalPrivacyControlEnabled, + ColorScheme? preferredColorScheme, + CookieBannerHandlingMode? cookieBannerHandlingMode, + CookieBannerHandlingMode? cookieBannerHandlingModePrivateBrowsing, + bool? cookieBannerHandlingGlobalRules, + bool? cookieBannerHandlingGlobalRulesSubFrames, + WebContentIsolationStrategy? webContentIsolationStrategy, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfEngineSettings.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfEngineSettings.copyWith.fieldName(...)` +class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy { + const _$EngineSettingsCWProxyImpl(this._value); + + final EngineSettings _value; + + @override + EngineSettings javascriptEnabled(bool? javascriptEnabled) => + this(javascriptEnabled: javascriptEnabled); + + @override + EngineSettings trackingProtectionPolicy( + TrackingProtectionPolicy? trackingProtectionPolicy) => + this(trackingProtectionPolicy: trackingProtectionPolicy); + + @override + EngineSettings httpsOnlyMode(HttpsOnlyMode? httpsOnlyMode) => + this(httpsOnlyMode: httpsOnlyMode); + + @override + EngineSettings globalPrivacyControlEnabled( + bool? globalPrivacyControlEnabled) => + this(globalPrivacyControlEnabled: globalPrivacyControlEnabled); + + @override + EngineSettings preferredColorScheme(ColorScheme? preferredColorScheme) => + this(preferredColorScheme: preferredColorScheme); + + @override + EngineSettings cookieBannerHandlingMode( + CookieBannerHandlingMode? cookieBannerHandlingMode) => + this(cookieBannerHandlingMode: cookieBannerHandlingMode); + + @override + EngineSettings cookieBannerHandlingModePrivateBrowsing( + CookieBannerHandlingMode? cookieBannerHandlingModePrivateBrowsing) => + this( + cookieBannerHandlingModePrivateBrowsing: + cookieBannerHandlingModePrivateBrowsing); + + @override + EngineSettings cookieBannerHandlingGlobalRules( + bool? cookieBannerHandlingGlobalRules) => + this(cookieBannerHandlingGlobalRules: cookieBannerHandlingGlobalRules); + + @override + EngineSettings cookieBannerHandlingGlobalRulesSubFrames( + bool? cookieBannerHandlingGlobalRulesSubFrames) => + this( + cookieBannerHandlingGlobalRulesSubFrames: + cookieBannerHandlingGlobalRulesSubFrames); + + @override + EngineSettings webContentIsolationStrategy( + WebContentIsolationStrategy? webContentIsolationStrategy) => + this(webContentIsolationStrategy: webContentIsolationStrategy); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `EngineSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// EngineSettings(...).copyWith(id: 12, name: "My name") + /// ```` + EngineSettings call({ + Object? javascriptEnabled = const $CopyWithPlaceholder(), + Object? trackingProtectionPolicy = const $CopyWithPlaceholder(), + Object? httpsOnlyMode = const $CopyWithPlaceholder(), + Object? globalPrivacyControlEnabled = const $CopyWithPlaceholder(), + Object? preferredColorScheme = const $CopyWithPlaceholder(), + Object? cookieBannerHandlingMode = const $CopyWithPlaceholder(), + Object? cookieBannerHandlingModePrivateBrowsing = + const $CopyWithPlaceholder(), + Object? cookieBannerHandlingGlobalRules = const $CopyWithPlaceholder(), + Object? cookieBannerHandlingGlobalRulesSubFrames = + const $CopyWithPlaceholder(), + Object? webContentIsolationStrategy = const $CopyWithPlaceholder(), + }) { + return EngineSettings( + javascriptEnabled: javascriptEnabled == const $CopyWithPlaceholder() + ? _value.javascriptEnabled + // ignore: cast_nullable_to_non_nullable + : javascriptEnabled as bool?, + trackingProtectionPolicy: + trackingProtectionPolicy == const $CopyWithPlaceholder() + ? _value.trackingProtectionPolicy + // ignore: cast_nullable_to_non_nullable + : trackingProtectionPolicy as TrackingProtectionPolicy?, + httpsOnlyMode: httpsOnlyMode == const $CopyWithPlaceholder() + ? _value.httpsOnlyMode + // ignore: cast_nullable_to_non_nullable + : httpsOnlyMode as HttpsOnlyMode?, + globalPrivacyControlEnabled: + globalPrivacyControlEnabled == const $CopyWithPlaceholder() + ? _value.globalPrivacyControlEnabled + // ignore: cast_nullable_to_non_nullable + : globalPrivacyControlEnabled as bool?, + preferredColorScheme: preferredColorScheme == const $CopyWithPlaceholder() + ? _value.preferredColorScheme + // ignore: cast_nullable_to_non_nullable + : preferredColorScheme as ColorScheme?, + cookieBannerHandlingMode: + cookieBannerHandlingMode == const $CopyWithPlaceholder() + ? _value.cookieBannerHandlingMode + // ignore: cast_nullable_to_non_nullable + : cookieBannerHandlingMode as CookieBannerHandlingMode?, + cookieBannerHandlingModePrivateBrowsing: + cookieBannerHandlingModePrivateBrowsing == + const $CopyWithPlaceholder() + ? _value.cookieBannerHandlingModePrivateBrowsing + // ignore: cast_nullable_to_non_nullable + : cookieBannerHandlingModePrivateBrowsing + as CookieBannerHandlingMode?, + cookieBannerHandlingGlobalRules: + cookieBannerHandlingGlobalRules == const $CopyWithPlaceholder() + ? _value.cookieBannerHandlingGlobalRules + // ignore: cast_nullable_to_non_nullable + : cookieBannerHandlingGlobalRules as bool?, + cookieBannerHandlingGlobalRulesSubFrames: + cookieBannerHandlingGlobalRulesSubFrames == + const $CopyWithPlaceholder() + ? _value.cookieBannerHandlingGlobalRulesSubFrames + // ignore: cast_nullable_to_non_nullable + : cookieBannerHandlingGlobalRulesSubFrames as bool?, + webContentIsolationStrategy: + webContentIsolationStrategy == const $CopyWithPlaceholder() + ? _value.webContentIsolationStrategy + // ignore: cast_nullable_to_non_nullable + : webContentIsolationStrategy as WebContentIsolationStrategy?, + ); + } +} + +extension $EngineSettingsCopyWith on EngineSettings { + /// Returns a callable class that can be used as follows: `instanceOfEngineSettings.copyWith(...)` or like so:`instanceOfEngineSettings.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$EngineSettingsCWProxy get copyWith => _$EngineSettingsCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +EngineSettings _$EngineSettingsFromJson(Map json) => + EngineSettings.withDefaults( + javascriptEnabled: json['javascriptEnabled'] as bool?, + trackingProtectionPolicy: $enumDecodeNullable( + _$TrackingProtectionPolicyEnumMap, json['trackingProtectionPolicy']), + httpsOnlyMode: + $enumDecodeNullable(_$HttpsOnlyModeEnumMap, json['httpsOnlyMode']), + globalPrivacyControlEnabled: json['globalPrivacyControlEnabled'] as bool?, + preferredColorScheme: $enumDecodeNullable( + _$ColorSchemeEnumMap, json['preferredColorScheme']), + cookieBannerHandlingMode: $enumDecodeNullable( + _$CookieBannerHandlingModeEnumMap, json['cookieBannerHandlingMode']), + cookieBannerHandlingModePrivateBrowsing: $enumDecodeNullable( + _$CookieBannerHandlingModeEnumMap, + json['cookieBannerHandlingModePrivateBrowsing']), + cookieBannerHandlingGlobalRules: + json['cookieBannerHandlingGlobalRules'] as bool?, + cookieBannerHandlingGlobalRulesSubFrames: + json['cookieBannerHandlingGlobalRulesSubFrames'] as bool?, + webContentIsolationStrategy: $enumDecodeNullable( + _$WebContentIsolationStrategyEnumMap, + json['webContentIsolationStrategy']), + ); + +Map _$EngineSettingsToJson(EngineSettings instance) => + { + 'javascriptEnabled': instance.javascriptEnabled, + 'trackingProtectionPolicy': + _$TrackingProtectionPolicyEnumMap[instance.trackingProtectionPolicy], + 'httpsOnlyMode': _$HttpsOnlyModeEnumMap[instance.httpsOnlyMode], + 'globalPrivacyControlEnabled': instance.globalPrivacyControlEnabled, + 'preferredColorScheme': + _$ColorSchemeEnumMap[instance.preferredColorScheme], + 'cookieBannerHandlingMode': + _$CookieBannerHandlingModeEnumMap[instance.cookieBannerHandlingMode], + 'cookieBannerHandlingModePrivateBrowsing': + _$CookieBannerHandlingModeEnumMap[ + instance.cookieBannerHandlingModePrivateBrowsing], + 'cookieBannerHandlingGlobalRules': + instance.cookieBannerHandlingGlobalRules, + 'cookieBannerHandlingGlobalRulesSubFrames': + instance.cookieBannerHandlingGlobalRulesSubFrames, + 'webContentIsolationStrategy': _$WebContentIsolationStrategyEnumMap[ + instance.webContentIsolationStrategy], + }; + +const _$TrackingProtectionPolicyEnumMap = { + TrackingProtectionPolicy.none: 'none', + TrackingProtectionPolicy.recommended: 'recommended', + TrackingProtectionPolicy.strict: 'strict', + TrackingProtectionPolicy.custom: 'custom', +}; + +const _$HttpsOnlyModeEnumMap = { + HttpsOnlyMode.disabled: 'disabled', + HttpsOnlyMode.privateOnly: 'privateOnly', + HttpsOnlyMode.enabled: 'enabled', +}; + +const _$ColorSchemeEnumMap = { + ColorScheme.system: 'system', + ColorScheme.light: 'light', + ColorScheme.dark: 'dark', +}; + +const _$CookieBannerHandlingModeEnumMap = { + CookieBannerHandlingMode.disabled: 'disabled', + CookieBannerHandlingMode.rejectAll: 'rejectAll', + CookieBannerHandlingMode.rejectOrAcceptAll: 'rejectOrAcceptAll', +}; + +const _$WebContentIsolationStrategyEnumMap = { + WebContentIsolationStrategy.isolateNothing: 'isolateNothing', + WebContentIsolationStrategy.isolateEverything: 'isolateEverything', + WebContentIsolationStrategy.isolateHighValue: 'isolateHighValue', +}; diff --git a/app/lib/presentation/widgets/selectable_chips.dart b/app/lib/presentation/widgets/selectable_chips.dart index f97b7498..9913a67c 100644 --- a/app/lib/presentation/widgets/selectable_chips.dart +++ b/app/lib/presentation/widgets/selectable_chips.dart @@ -29,6 +29,8 @@ class SelectableChips extends StatelessWidget { final Widget? Function(T item)? itemAvatar; final int? Function(T item)? itemBadgeCount; + final Widget Function(Widget child, S item)? itemWrap; + final void Function(T item)? onSelected; final void Function(T item)? onDeleted; @@ -37,6 +39,7 @@ class SelectableChips extends StatelessWidget { required this.itemLabel, this.itemAvatar, this.itemBadgeCount, + this.itemWrap, required this.availableItems, this.selectedItem, this.maxCount = 25, @@ -76,7 +79,7 @@ class SelectableChips extends StatelessWidget { itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; - return Padding( + final child = Padding( padding: const EdgeInsets.only(right: 8.0), child: _BadgeWrapper( count: itemBadgeCount?.call(item), @@ -101,6 +104,8 @@ class SelectableChips extends StatelessWidget { ), ), ); + + return (itemWrap != null) ? itemWrap!(child, item) : child; }, ); }, diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 98d5e86e..39294ee8 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -30,9 +30,11 @@ dependencies: flutter_mozilla_components: path: ../packages/flutter_mozilla_components flutter_pdf_text: ^0.8.1 + flutter_reorderable_grid_view: + path: ../../flutter-reorderable-grid-view flutter_secure_storage: ^10.0.0-beta.4 flutter_sharing_intent: ^1.1.1 - go_router: ^14.6.3 + go_router: ^14.7.1 google_fonts: ^6.2.1 home_widget: ^0.7.0 hooks_riverpod: ^2.6.1 @@ -54,7 +56,6 @@ dependencies: path_provider: ^2.1.5 pocketbase: ^0.21.0 provider: ^6.1.2 - reorderable_grid: ^1.0.10 riverpod: ^2.6.1 riverpod_annotation: ^2.6.1 rxdart: ^0.28.0 @@ -103,6 +104,7 @@ flutter: - assets/icon/ - assets/landing/ - assets/fonts/ + - assets/preferences/ flutter_launcher_icons: android: "launcher_icon" diff --git a/app/pubspec_overrides.yaml b/app/pubspec_overrides.yaml index e3b18934..f3567662 100644 --- a/app/pubspec_overrides.yaml +++ b/app/pubspec_overrides.yaml @@ -1,8 +1,8 @@ # melos_managed_dependency_overrides: flutter_mozilla_components,sqlite3_vec,flutter_chat_core dependency_overrides: - flutter_chat_core: - path: ../../flutter_chat_ui/packages/flutter_chat_core flutter_mozilla_components: path: ../packages/flutter_mozilla_components sqlite3_vec: path: ../packages/sqlite3_vec + flutter_chat_core: + path: ../../flutter_chat_ui/packages/flutter_chat_core diff --git a/packages/flutter_mozilla_components/android/build.gradle b/packages/flutter_mozilla_components/android/build.gradle index 0335fca7..15c28fcc 100644 --- a/packages/flutter_mozilla_components/android/build.gradle +++ b/packages/flutter_mozilla_components/android/build.gradle @@ -3,7 +3,7 @@ version = "1.0-SNAPSHOT" buildscript { ext.kotlin_version = "1.9.22" - ext.mozillaComponentsVersion = '134.0.20241030214633' + ext.mozillaComponentsVersion = '136.0.20250127162202' repositories { google() diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/api.js b/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/api.js new file mode 100644 index 00000000..295f5de6 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/api.js @@ -0,0 +1,119 @@ +"use strict"; + +const { BackgroundTasksUtils } = ChromeUtils.importESModule( + "resource://gre/modules/BackgroundTasksUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "log", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + let consoleOptions = { + // tip: set maxLogLevel to "debug" and use log.debug() to create detailed + // messages during development. See LOG_LEVELS in Console.sys.mjs for details. + maxLogLevel: "error", + maxLogLevelPref: "app.update.background.loglevel", + prefix: "PrefsManager", + }; + return new ConsoleAPI(consoleOptions); +}); + +var prefmanager = class extends ExtensionAPI { + getAPI(context) { + return { + experiments: { + prefmanager: { + async resetPrefs(prefNames) { + if (prefNames && prefNames.length > 0) { + for (const prefName of prefNames) { + Services.prefs.clearUserPref(prefName); + } + } else { + Services.prefs.resetPrefs(); + } + }, + async getPrefs(prefNames) { + const prefs = (prefNames && prefNames.length > 0) + ? prefNames + : Services.prefs.getChildList(""); + + const result = {}; + + for (const prefName of prefs) { + try { + switch (Services.prefs.getPrefType(prefName)) { + case Services.prefs.PREF_BOOL: + result[prefName] = Services.prefs.getBoolPref(prefName); + break; + case Services.prefs.PREF_INT: + result[prefName] = Services.prefs.getIntPref(prefName); + break; + case Services.prefs.PREF_STRING: + result[prefName] = Services.prefs.getCharPref(prefName); + break; + default: + // Skip complex values or invalid preferences + continue; + } + } catch (e) { + lazy.log.error(`Error reading preference ${prefName}: ${e}`); + continue; + } + } + + lazy.log.debug(`getAll: retrieved ${Object.keys(result).length} preferences`); + return result; + }, + async parsePrefsAndApply(prefsFileContent, predicate = null) { + let prefs = {}; + let addPref = (kind, name, value) => { + if (predicate && !predicate(name)) { + return; + } + prefs[name] = value; + }; + + Services.prefs.parsePrefsFromBuffer( + prefsFileContent, + { + onStringPref: addPref, + onIntPref: addPref, + onBoolPref: addPref, + onError(message) { + throw new Error( + `Error parsing preferences "${message}"` + ); + }, + }, + ); + + await BackgroundTasksUtils.withProfileLock(profileLock => { + for (let [name, value] of Object.entries(prefs)) { + switch (typeof value) { + case "boolean": + Services.prefs.setBoolPref(name, value); + break; + case "number": + Services.prefs.setIntPref(name, value); + break; + case "string": + Services.prefs.setCharPref(name, value); + break; + default: + throw new Error( + `Pref from default profile with name "${name}" has unrecognized type` + ); + } + } + }); + + lazy.log.debug(`applyPreferences: parsed prefs from buffer`, prefs); + return prefs; + } + } + } + }; + } +}; diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/background.js b/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/background.js new file mode 100644 index 00000000..a8cf37e2 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/background.js @@ -0,0 +1,47 @@ +'use strict'; + +const encoder = new TextEncoder(); + +const port = browser.runtime.connectNative("prefManager"); + +function sendJsonResultForRequest(id) { + return function (result) { + port.postMessage({ + "id": id, + "status": "success", + "result": result + }) + } +} + +function sendErrorForRequest(id) { + return function (error) { + console.error(error); + port.postMessage({ + "id": id, + "status": "error", + "error": error + }); + } +} + +port.onMessage.addListener(message => { + let requestId = message["id"] + switch (message["action"]) { + case "parsePrefsAndApply": + browser.experiments.prefmanager.parsePrefsAndApply(encoder.encode(message["args"]), null) + .then(sendJsonResultForRequest(requestId)) + .catch(sendErrorForRequest(requestId)) + break + case "getPrefs": + browser.experiments.prefmanager.getPrefs(message["args"]) + .then(sendJsonResultForRequest(requestId)) + .catch(sendErrorForRequest(requestId)) + break + case "resetPrefs": + browser.experiments.prefmanager.resetPrefs(message["args"]) + .then(sendJsonResultForRequest(requestId)) + .catch(sendErrorForRequest(requestId)) + break + } +}); diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/manifest.json b/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/manifest.json new file mode 100644 index 00000000..b8c913e2 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/manifest.json @@ -0,0 +1,39 @@ +{ + "manifest_version": 2, + "name": "Pref Manager", + "version": "1.0", + "description": "Manipulate GeckoView preferences", + "browser_specific_settings": { + "gecko": { + "id": "pref-manager@movenext.me" + } + }, + "background": { + "scripts": [ + "background.js" + ] + }, + "experiment_apis": { + "prefmanager": { + "schema": "schema.json", + "parent": { + "scopes": [ + "addon_parent" + ], + "paths": [ + [ + "experiments", + "prefmanager" + ] + ], + "script": "api.js" + } + } + }, + "permissions": [ + "nativeMessaging", + "nativeMessagingFromContent", + "geckoViewAddons", + "" + ] +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/schema.json b/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/schema.json new file mode 100644 index 00000000..6b2a279f --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/pref_manager/schema.json @@ -0,0 +1,78 @@ +[ + { + "namespace": "experiments.prefmanager", + "description": "Experimental API for updating Geckoview preferences", + "functions": [ + { + "name": "getPrefs", + "type": "function", + "description": "Retrieves preferences and their values", + "async": true, + "parameters": [ + { + "name": "prefNames", + "type": "array", + "optional": true, + "description": "Array of preference names to retrieve. If empty or null, retrieves all preferences", + "items": { + "type": "string" + } + } + ], + "returns": { + "type": "object", + "description": "Object containing the preferences as key-value pairs", + "additionalProperties": { + "type": "any", + "description": "Preference values can be boolean, number, or string" + } + } + }, + { + "name": "parsePrefsAndApply", + "type": "function", + "description": "Reads and sets preferences from an encoded preferences file content", + "async": true, + "parameters": [ + { + "name": "prefsFileContent", + "type": "object", + "isInstanceOf": "Uint8Array", + "description": "Encoded content of the preferences file to parse (Uint8Array)" + }, + { + "name": "predicate", + "type": "function", + "optional": true, + "description": "Optional filter function to determine which preferences to include" + } + ], + "returns": { + "type": "object", + "description": "Object containing the parsed preferences as key-value pairs", + "additionalProperties": { + "type": "any", + "description": "Preference values can be boolean, number, or string" + } + } + }, + { + "name": "resetPrefs", + "type": "function", + "description": "Resets multiple or all preferences to their default values", + "async": true, + "parameters": [ + { + "name": "prefNames", + "type": "array", + "optional": true, + "description": "Array of preference names to reset. If empty or null, resets all preferences", + "items": { + "type": "string" + } + } + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/EngineProvider.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/EngineProvider.kt index 338d5597..d67da059 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/EngineProvider.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/EngineProvider.kt @@ -6,6 +6,7 @@ package eu.lensai.flutter_mozilla_components import android.content.Context import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature +import eu.lensai.flutter_mozilla_components.feature.PrefManagerFeature import mozilla.components.browser.engine.gecko.GeckoEngine import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient import mozilla.components.concept.engine.DefaultSettings @@ -31,6 +32,7 @@ object EngineProvider { // About config it's no longer enabled by default builder.aboutConfigEnabled(true) + builder.extensionsProcessEnabled(true) builder.extensionsWebAPIEnabled(true) builder.consoleOutput(true) @@ -47,6 +49,7 @@ object EngineProvider { return GeckoEngine(context, defaultSettings, runtime).also { WebCompatFeature.install(it) CookieManagerFeature.install(it) + PrefManagerFeature.install(it) } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt index 04e33df2..66049b06 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt @@ -10,6 +10,7 @@ import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoFindApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl +import eu.lensai.flutter_mozilla_components.api.GeckoPrefApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerImpl import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoSuggestionApiImpl @@ -22,6 +23,7 @@ import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoFindApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi +import eu.lensai.flutter_mozilla_components.pigeons.GeckoPrefApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionController import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi @@ -91,6 +93,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl()) GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl()) GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl()) + GeckoPrefApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPrefApiImpl()) GeckoFindApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFindApiImpl()) GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl( selectionActionDelegate diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt index 1185780a..80c31d81 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt @@ -1,9 +1,19 @@ package eu.lensai.flutter_mozilla_components.api import eu.lensai.flutter_mozilla_components.GlobalComponents +import eu.lensai.flutter_mozilla_components.pigeons.ColorScheme +import eu.lensai.flutter_mozilla_components.pigeons.CookieBannerHandlingMode +import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettings import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi +import eu.lensai.flutter_mozilla_components.pigeons.HttpsOnlyMode +import eu.lensai.flutter_mozilla_components.pigeons.WebContentIsolationStrategy import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy +import mozilla.components.concept.engine.mediaquery.PreferredColorScheme import mozilla.components.feature.addons.logger +import mozilla.components.feature.session.SettingsUseCases +import mozilla.components.feature.session.TrackingProtectionUseCases /** * Implementation of GeckoEngineSettingsApi that manages engine-specific settings @@ -17,13 +27,99 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { requireNotNull(GlobalComponents.components) { "Components not initialized" } } - override fun javaScriptEnabled(state: Boolean) { - try { - components.core.engine.settings.javascriptEnabled = state - logger.debug("$TAG: JavaScript enabled state changed to: $state") - } catch (e: Exception) { - logger.error("$TAG: Failed to set JavaScript enabled state", e) - throw IllegalStateException("Failed to set JavaScript enabled state", e) + override fun setDefaultSettings(settings: GeckoEngineSettings) { + if(settings.javascriptEnabled != null) { + components.core.engineSettings.javascriptEnabled = settings.javascriptEnabled; + } + if(settings.trackingProtectionPolicy != null) { + components.core.engineSettings.trackingProtectionPolicy = when(settings.trackingProtectionPolicy) { + eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.NONE -> TrackingProtectionPolicy.none() + eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.RECOMMENDED -> TrackingProtectionPolicy.recommended() + eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.STRICT -> TrackingProtectionPolicy.strict() + eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.CUSTOM -> TODO() + } + } + if(settings.httpsOnlyMode != null) { + components.core.engineSettings.httpsOnlyMode = when(settings.httpsOnlyMode) { + HttpsOnlyMode.DISABLED -> Engine.HttpsOnlyMode.DISABLED + HttpsOnlyMode.PRIVATE_ONLY -> Engine.HttpsOnlyMode.ENABLED_PRIVATE_ONLY + HttpsOnlyMode.ENABLED -> Engine.HttpsOnlyMode.ENABLED + } + } + if(settings.globalPrivacyControlEnabled != null) { + components.core.engineSettings.globalPrivacyControlEnabled = settings.globalPrivacyControlEnabled; + } + if(settings.preferredColorScheme != null) { + components.core.engineSettings.preferredColorScheme = when(settings.preferredColorScheme) { + ColorScheme.SYSTEM -> PreferredColorScheme.System + ColorScheme.LIGHT -> PreferredColorScheme.Light + ColorScheme.DARK -> PreferredColorScheme.Dark + } + } + if(settings.cookieBannerHandlingMode != null) { + components.core.engineSettings.cookieBannerHandlingMode = when(settings.cookieBannerHandlingMode) { + CookieBannerHandlingMode.DISABLED -> EngineSession.CookieBannerHandlingMode.DISABLED + CookieBannerHandlingMode.REJECT_ALL -> EngineSession.CookieBannerHandlingMode.REJECT_ALL + CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL -> EngineSession.CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL + } + } + if(settings.cookieBannerHandlingModePrivateBrowsing != null) { + components.core.engineSettings.cookieBannerHandlingModePrivateBrowsing = when(settings.cookieBannerHandlingModePrivateBrowsing) { + CookieBannerHandlingMode.DISABLED -> EngineSession.CookieBannerHandlingMode.DISABLED + CookieBannerHandlingMode.REJECT_ALL -> EngineSession.CookieBannerHandlingMode.REJECT_ALL + CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL -> EngineSession.CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL + } + } + if(settings.cookieBannerHandlingGlobalRules != null) { + components.core.engineSettings.cookieBannerHandlingGlobalRules = settings.cookieBannerHandlingGlobalRules; + } + if(settings.cookieBannerHandlingGlobalRulesSubFrames != null) { + components.core.engineSettings.cookieBannerHandlingGlobalRulesSubFrames = settings.cookieBannerHandlingGlobalRulesSubFrames; + } + if(settings.webContentIsolationStrategy != null) { + components.core.engineSettings.webContentIsolationStrategy = when(settings.webContentIsolationStrategy) { + WebContentIsolationStrategy.ISOLATE_NOTHING -> mozilla.components.concept.engine.fission.WebContentIsolationStrategy.ISOLATE_NOTHING + WebContentIsolationStrategy.ISOLATE_EVERYTHING -> mozilla.components.concept.engine.fission.WebContentIsolationStrategy.ISOLATE_EVERYTHING + WebContentIsolationStrategy.ISOLATE_HIGH_VALUE -> mozilla.components.concept.engine.fission.WebContentIsolationStrategy.ISOLATE_HIGH_VALUE + } + } + } + + override fun updateRuntimeSettings(settings: GeckoEngineSettings) { + //First parse and set default values + setDefaultSettings(settings); + + //Then copy default settings into runtime + if(settings.javascriptEnabled != null) { + components.core.engine.settings.javascriptEnabled = components.core.engineSettings.javascriptEnabled + } + if(settings.trackingProtectionPolicy != null) { + components.useCases.settingsUseCases.updateTrackingProtection(components.core.engineSettings.trackingProtectionPolicy!!) + components.useCases.sessionUseCases.reload() + } + if(settings.httpsOnlyMode != null) { + components.core.engineSettings.httpsOnlyMode = components.core.engineSettings.httpsOnlyMode + } + if(settings.globalPrivacyControlEnabled != null) { + components.core.engineSettings.globalPrivacyControlEnabled = components.core.engineSettings.globalPrivacyControlEnabled + } + if(settings.preferredColorScheme != null) { + components.core.engineSettings.preferredColorScheme = components.core.engineSettings.preferredColorScheme + } + if(settings.cookieBannerHandlingMode != null) { + components.core.engineSettings.cookieBannerHandlingMode = components.core.engineSettings.cookieBannerHandlingMode + } + if(settings.cookieBannerHandlingModePrivateBrowsing != null) { + components.core.engineSettings.cookieBannerHandlingModePrivateBrowsing = components.core.engineSettings.cookieBannerHandlingModePrivateBrowsing + } + if(settings.cookieBannerHandlingGlobalRules != null) { + components.core.engineSettings.cookieBannerHandlingGlobalRules = components.core.engineSettings.cookieBannerHandlingGlobalRules + } + if(settings.cookieBannerHandlingGlobalRulesSubFrames != null) { + components.core.engineSettings.cookieBannerHandlingGlobalRulesSubFrames = components.core.engineSettings.cookieBannerHandlingGlobalRulesSubFrames + } + if(settings.webContentIsolationStrategy != null) { + components.core.engineSettings.webContentIsolationStrategy = components.core.engineSettings.webContentIsolationStrategy } } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoPrefApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoPrefApiImpl.kt new file mode 100644 index 00000000..6c28f299 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoPrefApiImpl.kt @@ -0,0 +1,73 @@ +package eu.lensai.flutter_mozilla_components.api + +import eu.lensai.flutter_mozilla_components.feature.PrefManagerFeature +import eu.lensai.flutter_mozilla_components.feature.ResultConsumer +import eu.lensai.flutter_mozilla_components.pigeons.GeckoPrefApi +import org.json.JSONObject +import org.json.JSONArray + +class GeckoPrefApiImpl : GeckoPrefApi { + private fun List?.toJson(): JSONArray { + return JSONArray().apply { + this@toJson?.forEach { put(it) } + } + } + + fun JSONObject.toMap(): Map { + val map = mutableMapOf() + val keys = this.keys() + + while (keys.hasNext()) { + val key = keys.next() + when (val value = this.get(key)) { + is JSONObject -> map[key] = value.toMap() +// is JSONArray -> map[key] = value.toList() +// JSONObject.NULL -> map[key] = null + else -> map[key] = value + } + } + + return map + } + + override fun getPrefs( + preferenceFilter: List?, + callback: (Result>) -> Unit + ) { + PrefManagerFeature.scheduleRequest("getPrefs", preferenceFilter.toJson(), object : ResultConsumer { + override fun success(result: JSONObject) { + callback(Result.success(result.getJSONObject("result").toMap())) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) + } + }) + } + + override fun applyPrefs(prefBuffer: String, callback: (Result>) -> Unit) { + PrefManagerFeature.scheduleRequest("parsePrefsAndApply", prefBuffer, object : ResultConsumer { + override fun success(result: JSONObject) { + callback(Result.success(result.getJSONObject("result").toMap())) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) + } + }) + } + + override fun resetPrefs(preferenceNames: List?, callback: (Result) -> Unit) { + PrefManagerFeature.scheduleRequest("resetPrefs", preferenceNames.toJson(), object : ResultConsumer { + override fun success(result: JSONObject) { + callback(Result.success(Unit)) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) + } + }) + } + + +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt index 2aa6709c..2872b4eb 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt @@ -2,6 +2,7 @@ package eu.lensai.flutter_mozilla_components.components import android.content.Context import android.content.SharedPreferences +import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import eu.lensai.flutter_mozilla_components.Components import eu.lensai.flutter_mozilla_components.interceptor.AppRequestInterceptor @@ -25,7 +26,10 @@ import mozilla.components.browser.thumbnails.ThumbnailsMiddleware import mozilla.components.browser.thumbnails.storage.ThumbnailStorage import mozilla.components.concept.engine.DefaultSettings import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy +import mozilla.components.concept.engine.fission.WebContentIsolationStrategy +import mozilla.components.concept.engine.mediaquery.PreferredColorScheme import mozilla.components.concept.fetch.Client import mozilla.components.feature.addons.AddonManager import mozilla.components.feature.addons.amo.AMOAddonsProvider @@ -54,20 +58,39 @@ class Core(private val context: Context, PreferenceManager.getDefaultSharedPreferences(context) } - private val engineSettings by lazy { - DefaultSettings().apply { + val engineSettings by lazy { + DefaultSettings( //historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage) - requestInterceptor = AppRequestInterceptor(context) - remoteDebuggingEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_remote_debugging), false) - testingModeEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_testing_mode), false) - historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage) - trackingProtectionPolicy = createTrackingProtectionPolicy(prefs) - httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED - globalPrivacyControlEnabled = prefs.getBoolean( - context.getPreferenceKey(R.string.pref_key_global_privacy_control), - false, - ) - } + requestInterceptor = AppRequestInterceptor(context), + historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage), + testingModeEnabled = false, + remoteDebuggingEnabled = false, + automaticFontSizeAdjustment = true, + fontInflationEnabled = true, + suspendMediaWhenInactive = false, + getDesktopMode = { + store.state.desktopMode + }, + enterpriseRootsEnabled = false, + emailTrackerBlockingPrivateBrowsing = true, +// clearColor = ContextCompat.getColor( +// context, +// R.color.fx_mobile_layer_color_1, +// ), + + trackingProtectionPolicy = createTrackingProtectionPolicy(TrackingProtectionPolicy.strict()), + //FP Protection is handled by trackingPolicy + //fingerprintingProtection + //fingerprintingProtectionPrivateBrowsing + httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED, + globalPrivacyControlEnabled = true, + preferredColorScheme = PreferredColorScheme.Dark, + cookieBannerHandlingMode = EngineSession.CookieBannerHandlingMode.REJECT_ALL, + cookieBannerHandlingModePrivateBrowsing = EngineSession.CookieBannerHandlingMode.REJECT_ALL, + cookieBannerHandlingGlobalRules = true, + cookieBannerHandlingGlobalRulesSubFrames = true, + webContentIsolationStrategy = WebContentIsolationStrategy.ISOLATE_HIGH_VALUE + ) } val engine: Engine by lazy { @@ -136,7 +159,17 @@ class Core(private val context: Context, // PromptMiddleware(), SessionPrioritizationMiddleware(), RecordingDevicesMiddleware(context, components.notificationsDelegate), - ) + EngineMiddleware.create(engine), + ) + EngineMiddleware.create( + engine, + // We are disabling automatic suspending of engine sessions under memory pressure. + // Instead we solely rely on GeckoView and the Android system to reclaim memory + // when needed. For details, see: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1752594 + // https://github.com/mozilla-mobile/fenix/issues/12731 + // https://github.com/mozilla-mobile/android-components/issues/11300 + // https://github.com/mozilla-mobile/android-components/issues/11653 + trimMemoryAutomatically = false, + ) ).apply { components.events.registerFlowEvents(this) @@ -191,11 +224,10 @@ class Core(private val context: Context, * @return the constructed tracking protection policy based on preferences. */ private fun createTrackingProtectionPolicy( - prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context), - normalMode: Boolean = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_tracking_protection_normal), true), - privateMode: Boolean = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_tracking_protection_private), true), + trackingPolicy: EngineSession. TrackingProtectionPolicyForSessionTypes, + normalMode: Boolean = true, + privateMode: Boolean = true, ): TrackingProtectionPolicy { - val trackingPolicy = TrackingProtectionPolicy.recommended() return when { normalMode && privateMode -> trackingPolicy normalMode && !privateMode -> trackingPolicy.forRegularSessionsOnly() diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/UseCases.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/UseCases.kt index 998744b1..0c5b0c0d 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/UseCases.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/UseCases.kt @@ -11,6 +11,7 @@ import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.feature.downloads.DownloadsUseCases import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SettingsUseCases +import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.feature.tabs.CustomTabsUseCases import mozilla.components.feature.tabs.TabsUseCases @@ -49,4 +50,6 @@ class UseCases( val customTabsUseCases: CustomTabsUseCases by lazy { CustomTabsUseCases(store, sessionUseCases.loadUrl) } val appLinksUseCases by lazy { AppLinksUseCases(context) } + + //val trackingProtectionUseCases by lazy { TrackingProtectionUseCases(store, engine) } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/PrefManagerFeature.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/PrefManagerFeature.kt new file mode 100644 index 00000000..2175ba0a --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/PrefManagerFeature.kt @@ -0,0 +1,100 @@ +package eu.lensai.flutter_mozilla_components.feature + +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import mozilla.components.concept.engine.webextension.MessageHandler +import mozilla.components.concept.engine.webextension.Port +import mozilla.components.concept.engine.webextension.WebExtensionRuntime +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.webextensions.WebExtensionController +import org.json.JSONObject + +object PrefManagerFeature { + private val logger = Logger("pref-manager") + + private const val PREF_MANAGER_REPORTER_EXTENSION_ID = "pref-manager@movenext.me" + private const val PREF_MANAGER_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/pref_manager/" + private const val PREF_MANAGER_REPORTER_MESSAGING_ID = "prefManager" + + private var nextRequestId: Int = 0 + private val requestHandlers = HashMap>() + private val mutex = Mutex() + + @VisibleForTesting + // This is an internal var to make it mutable for unit testing purposes only + internal var extensionController = WebExtensionController( + PREF_MANAGER_REPORTER_EXTENSION_ID, + PREF_MANAGER_REPORTER_EXTENSION_URL, + PREF_MANAGER_REPORTER_MESSAGING_ID, + ) + + fun scheduleRequest(command: String, args: Any, callback: ResultConsumer) { + val message = JSONObject() + message.put("action", command); + message.put("args", args) + + runBlocking { + withContext(Dispatchers.Default) { + mutex.withLock { + message.put("id", nextRequestId) + + requestHandlers[nextRequestId] = callback + + nextRequestId += 1 + + extensionController.sendBackgroundMessage(message) + } + } + } + } + + private class PrefManagerReporterBackgroundMessageHandler() : MessageHandler { + override fun onPortMessage(message: Any, port: Port) { + runBlocking { + withContext(Dispatchers.Default) { + mutex.withLock { + val messageJSON = message as JSONObject; + + val requestId = messageJSON.getInt("id") + val status = messageJSON.getString("status") + if (status == "success") { + requestHandlers[requestId]?.success(message) + } else { + requestHandlers[requestId]?.error( + "Pref Manager", + "Failed to perform operation", + message.getString("error") + ) + } + } + } + } + } + } + + /** + * Installs the web extension in the runtime through the WebExtensionRuntime install method + * + * @param runtime a WebExtensionRuntime. + * @param productName a custom product name used to automatically label reports. Defaults to + * "android-components". + */ + fun install(runtime: WebExtensionRuntime) { + extensionController.registerBackgroundMessageHandler( + PrefManagerReporterBackgroundMessageHandler(), + ) + extensionController.install( + runtime, + onSuccess = { + logger.debug("Installed PrefManager webextension: ${it.id}") + }, + onError = { throwable -> + logger.error("Failed to install PrefManager webextension: ", throwable) + }, + ) + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt index 1aaf5d84..52b97dc6 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.7.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -168,6 +168,67 @@ enum class GeckoSuggestionType(val raw: Int) { } } +enum class TrackingProtectionPolicy(val raw: Int) { + NONE(0), + RECOMMENDED(1), + STRICT(2), + CUSTOM(3); + + companion object { + fun ofRaw(raw: Int): TrackingProtectionPolicy? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class HttpsOnlyMode(val raw: Int) { + DISABLED(0), + PRIVATE_ONLY(1), + ENABLED(2); + + companion object { + fun ofRaw(raw: Int): HttpsOnlyMode? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class ColorScheme(val raw: Int) { + SYSTEM(0), + LIGHT(1), + DARK(2); + + companion object { + fun ofRaw(raw: Int): ColorScheme? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class CookieBannerHandlingMode(val raw: Int) { + DISABLED(0), + REJECT_ALL(1), + REJECT_OR_ACCEPT_ALL(2); + + companion object { + fun ofRaw(raw: Int): CookieBannerHandlingMode? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class WebContentIsolationStrategy(val raw: Int) { + ISOLATE_NOTHING(0), + ISOLATE_EVERYTHING(1), + ISOLATE_HIGH_VALUE(2); + + companion object { + fun ofRaw(raw: Int): WebContentIsolationStrategy? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** * Translation options that map to the Gecko Translations Options. * @@ -1051,6 +1112,51 @@ data class TabContent ( ) } } + +/** Generated class from Pigeon that represents data sent in messages. */ +data class GeckoEngineSettings ( + val javascriptEnabled: Boolean? = null, + val trackingProtectionPolicy: TrackingProtectionPolicy? = null, + val httpsOnlyMode: HttpsOnlyMode? = null, + val globalPrivacyControlEnabled: Boolean? = null, + val preferredColorScheme: ColorScheme? = null, + val cookieBannerHandlingMode: CookieBannerHandlingMode? = null, + val cookieBannerHandlingModePrivateBrowsing: CookieBannerHandlingMode? = null, + val cookieBannerHandlingGlobalRules: Boolean? = null, + val cookieBannerHandlingGlobalRulesSubFrames: Boolean? = null, + val webContentIsolationStrategy: WebContentIsolationStrategy? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): GeckoEngineSettings { + val javascriptEnabled = pigeonVar_list[0] as Boolean? + val trackingProtectionPolicy = pigeonVar_list[1] as TrackingProtectionPolicy? + val httpsOnlyMode = pigeonVar_list[2] as HttpsOnlyMode? + val globalPrivacyControlEnabled = pigeonVar_list[3] as Boolean? + val preferredColorScheme = pigeonVar_list[4] as ColorScheme? + val cookieBannerHandlingMode = pigeonVar_list[5] as CookieBannerHandlingMode? + val cookieBannerHandlingModePrivateBrowsing = pigeonVar_list[6] as CookieBannerHandlingMode? + val cookieBannerHandlingGlobalRules = pigeonVar_list[7] as Boolean? + val cookieBannerHandlingGlobalRulesSubFrames = pigeonVar_list[8] as Boolean? + val webContentIsolationStrategy = pigeonVar_list[9] as WebContentIsolationStrategy? + return GeckoEngineSettings(javascriptEnabled, trackingProtectionPolicy, httpsOnlyMode, globalPrivacyControlEnabled, preferredColorScheme, cookieBannerHandlingMode, cookieBannerHandlingModePrivateBrowsing, cookieBannerHandlingGlobalRules, cookieBannerHandlingGlobalRulesSubFrames, webContentIsolationStrategy) + } + } + fun toList(): List { + return listOf( + javascriptEnabled, + trackingProtectionPolicy, + httpsOnlyMode, + globalPrivacyControlEnabled, + preferredColorScheme, + cookieBannerHandlingMode, + cookieBannerHandlingModePrivateBrowsing, + cookieBannerHandlingGlobalRules, + cookieBannerHandlingGlobalRulesSubFrames, + webContentIsolationStrategy, + ) + } +} private open class GeckoPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -1095,140 +1201,170 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { } } 137.toByte() -> { - return (readValue(buffer) as? List)?.let { - TranslationOptions.fromList(it) + return (readValue(buffer) as Long?)?.let { + TrackingProtectionPolicy.ofRaw(it.toInt()) } } 138.toByte() -> { - return (readValue(buffer) as? List)?.let { - ReaderState.fromList(it) + return (readValue(buffer) as Long?)?.let { + HttpsOnlyMode.ofRaw(it.toInt()) } } 139.toByte() -> { - return (readValue(buffer) as? List)?.let { - LastMediaAccessState.fromList(it) + return (readValue(buffer) as Long?)?.let { + ColorScheme.ofRaw(it.toInt()) } } 140.toByte() -> { - return (readValue(buffer) as? List)?.let { - HistoryMetadataKey.fromList(it) + return (readValue(buffer) as Long?)?.let { + CookieBannerHandlingMode.ofRaw(it.toInt()) } } 141.toByte() -> { - return (readValue(buffer) as? List)?.let { - PackageCategoryValue.fromList(it) + return (readValue(buffer) as Long?)?.let { + WebContentIsolationStrategy.ofRaw(it.toInt()) } } 142.toByte() -> { return (readValue(buffer) as? List)?.let { - ExternalPackage.fromList(it) + TranslationOptions.fromList(it) } } 143.toByte() -> { return (readValue(buffer) as? List)?.let { - LoadUrlFlagsValue.fromList(it) + ReaderState.fromList(it) } } 144.toByte() -> { return (readValue(buffer) as? List)?.let { - SourceValue.fromList(it) + LastMediaAccessState.fromList(it) } } 145.toByte() -> { return (readValue(buffer) as? List)?.let { - TabState.fromList(it) + HistoryMetadataKey.fromList(it) } } 146.toByte() -> { return (readValue(buffer) as? List)?.let { - RecoverableTab.fromList(it) + PackageCategoryValue.fromList(it) } } 147.toByte() -> { return (readValue(buffer) as? List)?.let { - RecoverableBrowserState.fromList(it) + ExternalPackage.fromList(it) } } 148.toByte() -> { return (readValue(buffer) as? List)?.let { - IconRequest.fromList(it) + LoadUrlFlagsValue.fromList(it) } } 149.toByte() -> { return (readValue(buffer) as? List)?.let { - ResourceSize.fromList(it) + SourceValue.fromList(it) } } 150.toByte() -> { return (readValue(buffer) as? List)?.let { - Resource.fromList(it) + TabState.fromList(it) } } 151.toByte() -> { return (readValue(buffer) as? List)?.let { - IconResult.fromList(it) + RecoverableTab.fromList(it) } } 152.toByte() -> { return (readValue(buffer) as? List)?.let { - CookiePartitionKey.fromList(it) + RecoverableBrowserState.fromList(it) } } 153.toByte() -> { return (readValue(buffer) as? List)?.let { - Cookie.fromList(it) + IconRequest.fromList(it) } } 154.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryItem.fromList(it) + ResourceSize.fromList(it) } } 155.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryState.fromList(it) + Resource.fromList(it) } } 156.toByte() -> { return (readValue(buffer) as? List)?.let { - ReaderableState.fromList(it) + IconResult.fromList(it) } } 157.toByte() -> { return (readValue(buffer) as? List)?.let { - SecurityInfoState.fromList(it) + CookiePartitionKey.fromList(it) } } 158.toByte() -> { return (readValue(buffer) as? List)?.let { - TabContentState.fromList(it) + Cookie.fromList(it) } } 159.toByte() -> { return (readValue(buffer) as? List)?.let { - FindResultState.fromList(it) + HistoryItem.fromList(it) } } 160.toByte() -> { return (readValue(buffer) as? List)?.let { - CustomSelectionAction.fromList(it) + HistoryState.fromList(it) } } 161.toByte() -> { return (readValue(buffer) as? List)?.let { - WebExtensionData.fromList(it) + ReaderableState.fromList(it) } } 162.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoSuggestion.fromList(it) + SecurityInfoState.fromList(it) } } 163.toByte() -> { + return (readValue(buffer) as? List)?.let { + TabContentState.fromList(it) + } + } + 164.toByte() -> { + return (readValue(buffer) as? List)?.let { + FindResultState.fromList(it) + } + } + 165.toByte() -> { + return (readValue(buffer) as? List)?.let { + CustomSelectionAction.fromList(it) + } + } + 166.toByte() -> { + return (readValue(buffer) as? List)?.let { + WebExtensionData.fromList(it) + } + } + 167.toByte() -> { + return (readValue(buffer) as? List)?.let { + GeckoSuggestion.fromList(it) + } + } + 168.toByte() -> { return (readValue(buffer) as? List)?.let { TabContent.fromList(it) } } + 169.toByte() -> { + return (readValue(buffer) as? List)?.let { + GeckoEngineSettings.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -1266,114 +1402,138 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { stream.write(136) writeValue(stream, value.raw) } - is TranslationOptions -> { + is TrackingProtectionPolicy -> { stream.write(137) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is ReaderState -> { + is HttpsOnlyMode -> { stream.write(138) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is LastMediaAccessState -> { + is ColorScheme -> { stream.write(139) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is HistoryMetadataKey -> { + is CookieBannerHandlingMode -> { stream.write(140) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is PackageCategoryValue -> { + is WebContentIsolationStrategy -> { stream.write(141) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is ExternalPackage -> { + is TranslationOptions -> { stream.write(142) writeValue(stream, value.toList()) } - is LoadUrlFlagsValue -> { + is ReaderState -> { stream.write(143) writeValue(stream, value.toList()) } - is SourceValue -> { + is LastMediaAccessState -> { stream.write(144) writeValue(stream, value.toList()) } - is TabState -> { + is HistoryMetadataKey -> { stream.write(145) writeValue(stream, value.toList()) } - is RecoverableTab -> { + is PackageCategoryValue -> { stream.write(146) writeValue(stream, value.toList()) } - is RecoverableBrowserState -> { + is ExternalPackage -> { stream.write(147) writeValue(stream, value.toList()) } - is IconRequest -> { + is LoadUrlFlagsValue -> { stream.write(148) writeValue(stream, value.toList()) } - is ResourceSize -> { + is SourceValue -> { stream.write(149) writeValue(stream, value.toList()) } - is Resource -> { + is TabState -> { stream.write(150) writeValue(stream, value.toList()) } - is IconResult -> { + is RecoverableTab -> { stream.write(151) writeValue(stream, value.toList()) } - is CookiePartitionKey -> { + is RecoverableBrowserState -> { stream.write(152) writeValue(stream, value.toList()) } - is Cookie -> { + is IconRequest -> { stream.write(153) writeValue(stream, value.toList()) } - is HistoryItem -> { + is ResourceSize -> { stream.write(154) writeValue(stream, value.toList()) } - is HistoryState -> { + is Resource -> { stream.write(155) writeValue(stream, value.toList()) } - is ReaderableState -> { + is IconResult -> { stream.write(156) writeValue(stream, value.toList()) } - is SecurityInfoState -> { + is CookiePartitionKey -> { stream.write(157) writeValue(stream, value.toList()) } - is TabContentState -> { + is Cookie -> { stream.write(158) writeValue(stream, value.toList()) } - is FindResultState -> { + is HistoryItem -> { stream.write(159) writeValue(stream, value.toList()) } - is CustomSelectionAction -> { + is HistoryState -> { stream.write(160) writeValue(stream, value.toList()) } - is WebExtensionData -> { + is ReaderableState -> { stream.write(161) writeValue(stream, value.toList()) } - is GeckoSuggestion -> { + is SecurityInfoState -> { stream.write(162) writeValue(stream, value.toList()) } - is TabContent -> { + is TabContentState -> { stream.write(163) writeValue(stream, value.toList()) } + is FindResultState -> { + stream.write(164) + writeValue(stream, value.toList()) + } + is CustomSelectionAction -> { + stream.write(165) + writeValue(stream, value.toList()) + } + is WebExtensionData -> { + stream.write(166) + writeValue(stream, value.toList()) + } + is GeckoSuggestion -> { + stream.write(167) + writeValue(stream, value.toList()) + } + is TabContent -> { + stream.write(168) + writeValue(stream, value.toList()) + } + is GeckoEngineSettings -> { + stream.write(169) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -1433,7 +1593,8 @@ interface GeckoBrowserApi { } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface GeckoEngineSettingsApi { - fun javaScriptEnabled(state: Boolean) + fun setDefaultSettings(settings: GeckoEngineSettings) + fun updateRuntimeSettings(settings: GeckoEngineSettings) companion object { /** The codec used by GeckoEngineSettingsApi. */ @@ -1445,13 +1606,31 @@ interface GeckoEngineSettingsApi { fun setUp(binaryMessenger: BinaryMessenger, api: GeckoEngineSettingsApi?, messageChannelSuffix: String = "") { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.javaScriptEnabled$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setDefaultSettings$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List - val stateArg = args[0] as Boolean + val settingsArg = args[0] as GeckoEngineSettings val wrapped: List = try { - api.javaScriptEnabled(stateArg) + api.setDefaultSettings(settingsArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.updateRuntimeSettings$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val settingsArg = args[0] as GeckoEngineSettings + val wrapped: List = try { + api.updateRuntimeSettings(settingsArg) listOf(null) } catch (exception: Throwable) { wrapError(exception) @@ -2271,6 +2450,83 @@ interface GeckoIconsApi { } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface GeckoPrefApi { + fun getPrefs(preferenceFilter: List?, callback: (Result>) -> Unit) + fun applyPrefs(prefBuffer: String, callback: (Result>) -> Unit) + fun resetPrefs(preferenceNames: List?, callback: (Result) -> Unit) + + companion object { + /** The codec used by GeckoPrefApi. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoPrefApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoPrefApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.getPrefs$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val preferenceFilterArg = args[0] as List? + api.getPrefs(preferenceFilterArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.applyPrefs$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val prefBufferArg = args[0] as String + api.applyPrefs(prefBufferArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.resetPrefs$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val preferenceNamesArg = args[0] as List? + api.resetPrefs(preferenceNamesArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface GeckoCookieApi { fun getCookie(firstPartyDomain: String?, name: String, partitionKey: CookiePartitionKey?, storeId: String?, url: String, callback: (Result) -> Unit) fun getAllCookies(domain: String?, firstPartyDomain: String?, name: String?, partitionKey: CookiePartitionKey?, storeId: String?, url: String, callback: (Result>) -> Unit) diff --git a/packages/flutter_mozilla_components/android/src/main/res/values/preference_keys.xml b/packages/flutter_mozilla_components/android/src/main/res/values/preference_keys.xml index 93ca8672..f7458897 100644 --- a/packages/flutter_mozilla_components/android/src/main/res/values/preference_keys.xml +++ b/packages/flutter_mozilla_components/android/src/main/res/values/preference_keys.xml @@ -22,6 +22,8 @@ pref_key_tracking_protection_normal pref_key_tracking_protection_private pref_key_global_privacy_control + pref_key_fingerprinting_protection + pref_key_enterprise_roots_enabled pref_key_launch_external_app pref_key_override_amo_collection pref_key_override_amo_user diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart index 9555c8c1..4bf0e46c 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -8,6 +8,7 @@ export 'src/domain/services/gecko_engine_settings.dart'; export 'src/domain/services/gecko_event.dart'; export 'src/domain/services/gecko_find_in_page.dart'; export 'src/domain/services/gecko_icon.dart'; +export 'src/domain/services/gecko_pref.dart'; export 'src/domain/services/gecko_readerable.dart'; export 'src/domain/services/gecko_selection_action.dart'; export 'src/domain/services/gecko_session.dart'; @@ -17,10 +18,14 @@ export 'src/domain/services/gecko_tab_content.dart'; export 'src/geckoview_widget.dart'; export 'src/pigeons/gecko.g.dart' show + ColorScheme, + CookieBannerHandlingMode, CookieSameSiteStatus, + GeckoEngineSettings, GeckoSuggestion, GeckoSuggestionType, HistoryMetadataKey, + HttpsOnlyMode, IconSource, IconType, Resource, @@ -28,5 +33,7 @@ export 'src/pigeons/gecko.g.dart' SecurityInfoState, TabContent, TabContentState, + TrackingProtectionPolicy, + WebContentIsolationStrategy, WebExtensionActionType, WebExtensionData; diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_pref.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_pref.dart new file mode 100644 index 00000000..6a6341df --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_pref.dart @@ -0,0 +1,32 @@ +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; + +final _apiInstance = GeckoPrefApi(); + +class GeckoPrefService { + Future> getAllPrefs() { + return _apiInstance.getPrefs(null); + } + + Future> getPrefs(List prefs) { + return _apiInstance.getPrefs(prefs); + } + + Future> applyPrefs(Map prefs) { + final buffer = prefs.entries.map((pref) { + final value = switch (pref.value) { + final bool x => '$x', + final int x => '$x', + final String x => '"$x"', + _ => throw Exception('Unknow pref type') + }; + + return 'user_pref("${pref.key}", $value);'; + }).join(); + + return _apiInstance.applyPrefs(buffer); + } + + Future resetPrefs(List prefs) { + return _apiInstance.resetPrefs(prefs); + } +} diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 161b6e2a..a08b891c 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.7.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -94,6 +94,37 @@ enum GeckoSuggestionType { history, } +enum TrackingProtectionPolicy { + none, + recommended, + strict, + custom, +} + +enum HttpsOnlyMode { + disabled, + privateOnly, + enabled, +} + +enum ColorScheme { + system, + light, + dark, +} + +enum CookieBannerHandlingMode { + disabled, + rejectAll, + rejectOrAcceptAll, +} + +enum WebContentIsolationStrategy { + isolateNothing, + isolateEverything, + isolateHighValue, +} + /// Translation options that map to the Gecko Translations Options. /// /// @property downloadModel If the necessary models should be downloaded on request. If false, then @@ -1176,6 +1207,72 @@ class TabContent { } } +class GeckoEngineSettings { + GeckoEngineSettings({ + this.javascriptEnabled, + this.trackingProtectionPolicy, + this.httpsOnlyMode, + this.globalPrivacyControlEnabled, + this.preferredColorScheme, + this.cookieBannerHandlingMode, + this.cookieBannerHandlingModePrivateBrowsing, + this.cookieBannerHandlingGlobalRules, + this.cookieBannerHandlingGlobalRulesSubFrames, + this.webContentIsolationStrategy, + }); + + bool? javascriptEnabled; + + TrackingProtectionPolicy? trackingProtectionPolicy; + + HttpsOnlyMode? httpsOnlyMode; + + bool? globalPrivacyControlEnabled; + + ColorScheme? preferredColorScheme; + + CookieBannerHandlingMode? cookieBannerHandlingMode; + + CookieBannerHandlingMode? cookieBannerHandlingModePrivateBrowsing; + + bool? cookieBannerHandlingGlobalRules; + + bool? cookieBannerHandlingGlobalRulesSubFrames; + + WebContentIsolationStrategy? webContentIsolationStrategy; + + Object encode() { + return [ + javascriptEnabled, + trackingProtectionPolicy, + httpsOnlyMode, + globalPrivacyControlEnabled, + preferredColorScheme, + cookieBannerHandlingMode, + cookieBannerHandlingModePrivateBrowsing, + cookieBannerHandlingGlobalRules, + cookieBannerHandlingGlobalRulesSubFrames, + webContentIsolationStrategy, + ]; + } + + static GeckoEngineSettings decode(Object result) { + result as List; + return GeckoEngineSettings( + javascriptEnabled: result[0] as bool?, + trackingProtectionPolicy: result[1] as TrackingProtectionPolicy?, + httpsOnlyMode: result[2] as HttpsOnlyMode?, + globalPrivacyControlEnabled: result[3] as bool?, + preferredColorScheme: result[4] as ColorScheme?, + cookieBannerHandlingMode: result[5] as CookieBannerHandlingMode?, + cookieBannerHandlingModePrivateBrowsing: result[6] as CookieBannerHandlingMode?, + cookieBannerHandlingGlobalRules: result[7] as bool?, + cookieBannerHandlingGlobalRulesSubFrames: result[8] as bool?, + webContentIsolationStrategy: result[9] as WebContentIsolationStrategy?, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @@ -1208,87 +1305,105 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is GeckoSuggestionType) { buffer.putUint8(136); writeValue(buffer, value.index); - } else if (value is TranslationOptions) { + } else if (value is TrackingProtectionPolicy) { buffer.putUint8(137); - writeValue(buffer, value.encode()); - } else if (value is ReaderState) { + writeValue(buffer, value.index); + } else if (value is HttpsOnlyMode) { buffer.putUint8(138); - writeValue(buffer, value.encode()); - } else if (value is LastMediaAccessState) { + writeValue(buffer, value.index); + } else if (value is ColorScheme) { buffer.putUint8(139); - writeValue(buffer, value.encode()); - } else if (value is HistoryMetadataKey) { + writeValue(buffer, value.index); + } else if (value is CookieBannerHandlingMode) { buffer.putUint8(140); - writeValue(buffer, value.encode()); - } else if (value is PackageCategoryValue) { + writeValue(buffer, value.index); + } else if (value is WebContentIsolationStrategy) { buffer.putUint8(141); - writeValue(buffer, value.encode()); - } else if (value is ExternalPackage) { + writeValue(buffer, value.index); + } else if (value is TranslationOptions) { buffer.putUint8(142); writeValue(buffer, value.encode()); - } else if (value is LoadUrlFlagsValue) { + } else if (value is ReaderState) { buffer.putUint8(143); writeValue(buffer, value.encode()); - } else if (value is SourceValue) { + } else if (value is LastMediaAccessState) { buffer.putUint8(144); writeValue(buffer, value.encode()); - } else if (value is TabState) { + } else if (value is HistoryMetadataKey) { buffer.putUint8(145); writeValue(buffer, value.encode()); - } else if (value is RecoverableTab) { + } else if (value is PackageCategoryValue) { buffer.putUint8(146); writeValue(buffer, value.encode()); - } else if (value is RecoverableBrowserState) { + } else if (value is ExternalPackage) { buffer.putUint8(147); writeValue(buffer, value.encode()); - } else if (value is IconRequest) { + } else if (value is LoadUrlFlagsValue) { buffer.putUint8(148); writeValue(buffer, value.encode()); - } else if (value is ResourceSize) { + } else if (value is SourceValue) { buffer.putUint8(149); writeValue(buffer, value.encode()); - } else if (value is Resource) { + } else if (value is TabState) { buffer.putUint8(150); writeValue(buffer, value.encode()); - } else if (value is IconResult) { + } else if (value is RecoverableTab) { buffer.putUint8(151); writeValue(buffer, value.encode()); - } else if (value is CookiePartitionKey) { + } else if (value is RecoverableBrowserState) { buffer.putUint8(152); writeValue(buffer, value.encode()); - } else if (value is Cookie) { + } else if (value is IconRequest) { buffer.putUint8(153); writeValue(buffer, value.encode()); - } else if (value is HistoryItem) { + } else if (value is ResourceSize) { buffer.putUint8(154); writeValue(buffer, value.encode()); - } else if (value is HistoryState) { + } else if (value is Resource) { buffer.putUint8(155); writeValue(buffer, value.encode()); - } else if (value is ReaderableState) { + } else if (value is IconResult) { buffer.putUint8(156); writeValue(buffer, value.encode()); - } else if (value is SecurityInfoState) { + } else if (value is CookiePartitionKey) { buffer.putUint8(157); writeValue(buffer, value.encode()); - } else if (value is TabContentState) { + } else if (value is Cookie) { buffer.putUint8(158); writeValue(buffer, value.encode()); - } else if (value is FindResultState) { + } else if (value is HistoryItem) { buffer.putUint8(159); writeValue(buffer, value.encode()); - } else if (value is CustomSelectionAction) { + } else if (value is HistoryState) { buffer.putUint8(160); writeValue(buffer, value.encode()); - } else if (value is WebExtensionData) { + } else if (value is ReaderableState) { buffer.putUint8(161); writeValue(buffer, value.encode()); - } else if (value is GeckoSuggestion) { + } else if (value is SecurityInfoState) { buffer.putUint8(162); writeValue(buffer, value.encode()); - } else if (value is TabContent) { + } else if (value is TabContentState) { buffer.putUint8(163); writeValue(buffer, value.encode()); + } else if (value is FindResultState) { + buffer.putUint8(164); + writeValue(buffer, value.encode()); + } else if (value is CustomSelectionAction) { + buffer.putUint8(165); + writeValue(buffer, value.encode()); + } else if (value is WebExtensionData) { + buffer.putUint8(166); + writeValue(buffer, value.encode()); + } else if (value is GeckoSuggestion) { + buffer.putUint8(167); + writeValue(buffer, value.encode()); + } else if (value is TabContent) { + buffer.putUint8(168); + writeValue(buffer, value.encode()); + } else if (value is GeckoEngineSettings) { + buffer.putUint8(169); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1322,59 +1437,76 @@ class _PigeonCodec extends StandardMessageCodec { final int? value = readValue(buffer) as int?; return value == null ? null : GeckoSuggestionType.values[value]; case 137: - return TranslationOptions.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : TrackingProtectionPolicy.values[value]; case 138: - return ReaderState.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : HttpsOnlyMode.values[value]; case 139: - return LastMediaAccessState.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : ColorScheme.values[value]; case 140: - return HistoryMetadataKey.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : CookieBannerHandlingMode.values[value]; case 141: - return PackageCategoryValue.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : WebContentIsolationStrategy.values[value]; case 142: - return ExternalPackage.decode(readValue(buffer)!); + return TranslationOptions.decode(readValue(buffer)!); case 143: - return LoadUrlFlagsValue.decode(readValue(buffer)!); + return ReaderState.decode(readValue(buffer)!); case 144: - return SourceValue.decode(readValue(buffer)!); + return LastMediaAccessState.decode(readValue(buffer)!); case 145: - return TabState.decode(readValue(buffer)!); + return HistoryMetadataKey.decode(readValue(buffer)!); case 146: - return RecoverableTab.decode(readValue(buffer)!); + return PackageCategoryValue.decode(readValue(buffer)!); case 147: - return RecoverableBrowserState.decode(readValue(buffer)!); + return ExternalPackage.decode(readValue(buffer)!); case 148: - return IconRequest.decode(readValue(buffer)!); + return LoadUrlFlagsValue.decode(readValue(buffer)!); case 149: - return ResourceSize.decode(readValue(buffer)!); + return SourceValue.decode(readValue(buffer)!); case 150: - return Resource.decode(readValue(buffer)!); + return TabState.decode(readValue(buffer)!); case 151: - return IconResult.decode(readValue(buffer)!); + return RecoverableTab.decode(readValue(buffer)!); case 152: - return CookiePartitionKey.decode(readValue(buffer)!); + return RecoverableBrowserState.decode(readValue(buffer)!); case 153: - return Cookie.decode(readValue(buffer)!); + return IconRequest.decode(readValue(buffer)!); case 154: - return HistoryItem.decode(readValue(buffer)!); + return ResourceSize.decode(readValue(buffer)!); case 155: - return HistoryState.decode(readValue(buffer)!); + return Resource.decode(readValue(buffer)!); case 156: - return ReaderableState.decode(readValue(buffer)!); + return IconResult.decode(readValue(buffer)!); case 157: - return SecurityInfoState.decode(readValue(buffer)!); + return CookiePartitionKey.decode(readValue(buffer)!); case 158: - return TabContentState.decode(readValue(buffer)!); + return Cookie.decode(readValue(buffer)!); case 159: - return FindResultState.decode(readValue(buffer)!); + return HistoryItem.decode(readValue(buffer)!); case 160: - return CustomSelectionAction.decode(readValue(buffer)!); + return HistoryState.decode(readValue(buffer)!); case 161: - return WebExtensionData.decode(readValue(buffer)!); + return ReaderableState.decode(readValue(buffer)!); case 162: - return GeckoSuggestion.decode(readValue(buffer)!); + return SecurityInfoState.decode(readValue(buffer)!); case 163: + return TabContentState.decode(readValue(buffer)!); + case 164: + return FindResultState.decode(readValue(buffer)!); + case 165: + return CustomSelectionAction.decode(readValue(buffer)!); + case 166: + return WebExtensionData.decode(readValue(buffer)!); + case 167: + return GeckoSuggestion.decode(readValue(buffer)!); + case 168: return TabContent.decode(readValue(buffer)!); + case 169: + return GeckoEngineSettings.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -1452,15 +1584,37 @@ class GeckoEngineSettingsApi { final String pigeonVar_messageChannelSuffix; - Future javaScriptEnabled(bool state) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.javaScriptEnabled$pigeonVar_messageChannelSuffix'; + Future setDefaultSettings(GeckoEngineSettings settings) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setDefaultSettings$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([state]) as List?; + await pigeonVar_channel.send([settings]) as List?; + 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 updateRuntimeSettings(GeckoEngineSettings settings) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.updateRuntimeSettings$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([settings]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -2378,6 +2532,96 @@ class GeckoIconsApi { } } +class GeckoPrefApi { + /// Constructor for [GeckoPrefApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeckoPrefApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future> getPrefs(List? preferenceFilter) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.getPrefs$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([preferenceFilter]) as List?; + 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 if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as Map?)!.cast(); + } + } + + Future> applyPrefs(String prefBuffer) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.applyPrefs$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([prefBuffer]) as List?; + 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 if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as Map?)!.cast(); + } + } + + Future resetPrefs(List? preferenceNames) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.resetPrefs$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([preferenceNames]) as List?; + 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; + } + } +} + class GeckoCookieApi { /// Constructor for [GeckoCookieApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index a6cba69e..d3cb68df 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -539,6 +539,63 @@ class TabContent { ); } +enum TrackingProtectionPolicy { + none, + recommended, + strict, + custom, +} + +enum HttpsOnlyMode { + disabled, + privateOnly, + enabled, +} + +enum ColorScheme { + system, + light, + dark, +} + +enum CookieBannerHandlingMode { + disabled, + rejectAll, + rejectOrAcceptAll, +} + +enum WebContentIsolationStrategy { + isolateNothing, + isolateEverything, + isolateHighValue, +} + +class GeckoEngineSettings { + final bool? javascriptEnabled; + final TrackingProtectionPolicy? trackingProtectionPolicy; + final HttpsOnlyMode? httpsOnlyMode; + final bool? globalPrivacyControlEnabled; + final ColorScheme? preferredColorScheme; + final CookieBannerHandlingMode? cookieBannerHandlingMode; + final CookieBannerHandlingMode? cookieBannerHandlingModePrivateBrowsing; + final bool? cookieBannerHandlingGlobalRules; + final bool? cookieBannerHandlingGlobalRulesSubFrames; + final WebContentIsolationStrategy? webContentIsolationStrategy; + + GeckoEngineSettings( + this.javascriptEnabled, + this.trackingProtectionPolicy, + this.httpsOnlyMode, + this.globalPrivacyControlEnabled, + this.preferredColorScheme, + this.cookieBannerHandlingMode, + this.cookieBannerHandlingModePrivateBrowsing, + this.cookieBannerHandlingGlobalRules, + this.cookieBannerHandlingGlobalRulesSubFrames, + this.webContentIsolationStrategy, + ); +} + // /// Represents all the different supported types of data that can be found from long clicking // /// an element. // sealed class HitResult { @@ -614,7 +671,8 @@ abstract class GeckoBrowserApi { @HostApi() abstract class GeckoEngineSettingsApi { - void javaScriptEnabled(bool state); + void setDefaultSettings(GeckoEngineSettings settings); + void updateRuntimeSettings(GeckoEngineSettings settings); } @HostApi() @@ -804,6 +862,16 @@ abstract class GeckoIconsApi { IconResult loadIcon(IconRequest request); } +@HostApi() +abstract class GeckoPrefApi { + @async + Map getPrefs(List? preferenceFilter); + @async + Map applyPrefs(String prefBuffer); + @async + void resetPrefs(List? preferenceNames); +} + @HostApi() abstract class GeckoCookieApi { @async diff --git a/packages/flutter_mozilla_components/pubspec.yaml b/packages/flutter_mozilla_components/pubspec.yaml index 73a02b97..41070385 100644 --- a/packages/flutter_mozilla_components/pubspec.yaml +++ b/packages/flutter_mozilla_components/pubspec.yaml @@ -17,7 +17,7 @@ dev_dependencies: flutter_test: sdk: flutter lint: ^2.3.0 - pigeon: ^22.7.3 + pigeon: ^22.7.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/sqlite3_vec/example/.flutter-plugins-dependencies b/packages/sqlite3_vec/example/.flutter-plugins-dependencies index 05cff735..88b6b9c0 100644 --- a/packages/sqlite3_vec/example/.flutter-plugins-dependencies +++ b/packages/sqlite3_vec/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"macos":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"windows":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"sqlite3_flutter_libs","dependencies":[]},{"name":"sqlite3_vec","dependencies":[]}],"date_created":"2025-01-22 22:25:30.421767","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"macos":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"windows":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"sqlite3_flutter_libs","dependencies":[]},{"name":"sqlite3_vec","dependencies":[]}],"date_created":"2025-01-26 00:44:40.809594","version":"3.27.3","swift_package_manager_enabled":false} \ No newline at end of file