intermediate prefs

This commit is contained in:
Fabian Freund
2025-01-30 10:46:38 +01:00
parent 78c17a70c5
commit 36acd46318
51 changed files with 3744 additions and 1719 deletions
@@ -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';
@@ -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';
@@ -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<TabDragData>(
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)
@@ -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';
@@ -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),
),
),
],
@@ -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<String, PreferenceSetting> 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<Object?> 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<String, dynamic> json) =>
_$PreferenceSettingFromJson(json);
Map<String, dynamic> toJson() => _$PreferenceSettingToJson(this);
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [
value,
actualValue,
title,
description,
requireUserOptIn,
shouldBeDefault,
];
}
@@ -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<String, PreferenceSetting> 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<String, PreferenceSetting> 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<String, PreferenceSetting> 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<String, PreferenceSetting>,
);
}
}
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<String, dynamic> 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<String, dynamic> _$PreferenceSettingToJson(PreferenceSetting instance) =>
<String, dynamic>{
'value': instance.value,
'title': instance.title,
'description': instance.description,
'requireUserOptIn': instance.requireUserOptIn,
'shouldBeDefault': instance.shouldBeDefault,
};
@@ -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<Map<String, PreferenceSettingGroup>> _preferenceSettingGroups(
Ref ref,
) async {
final content = await rootBundle
.loadString('assets/preferences/settings.json')
.then(jsonDecode) as Map<String, dynamic>;
final userContent = content['user'] as Map<String, dynamic>;
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<String, dynamic>).map(
(key, value) => MapEntry(
key,
PreferenceSetting.fromJson(value as Map<String, dynamic>),
),
),
),
),
);
}
@Riverpod(keepAlive: true)
Future<PreferenceSettingGroup> _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<Map<String, Object>> _prefSubject;
Future<void> _updatePrefs() async {
final prefs = await _prefManager.getAllPrefs();
_prefSubject.add(prefs);
}
Future<void> applyPrefs(Map<String, Object> prefs) async {
await _prefManager.applyPrefs(prefs);
await _updatePrefs();
}
Future<void> resetPrefs(List<String> prefNames) async {
await _prefManager.resetPrefs(prefNames);
await _updatePrefs();
}
@override
Raw<Stream<Map<String, Object>>> build() {
_prefSubject = BehaviorSubject<Map<String, Object>>();
ref.onDispose(() {
unawaited(_prefSubject.close());
});
ref.onAddListener(() async {
await _updatePrefs();
});
return _prefSubject.stream;
}
}
@Riverpod()
class PreferenceSettingsGeneralRepository
extends _$PreferenceSettingsGeneralRepository {
late Map<String, PreferenceSettingGroup> _statelessGroups;
Future<void> 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<void> reset() async {
final prefs = _statelessGroups.values
.map((group) => group.settings.keys)
.flattened
.toList();
await ref.read(_preferenceRepositoryProvider.notifier).resetPrefs(prefs);
}
@override
Stream<Map<String, PreferenceSettingGroup>> 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<void> apply({List<String>? 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<void> reset({List<String>? 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<PreferenceSettingGroup> 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])),
),
),
);
}
}
@@ -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<Map<String, PreferenceSettingGroup>>.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<Map<String, PreferenceSettingGroup>>;
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<AsyncValue<PreferenceSettingGroup>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'_preferenceSettingGroupProvider';
}
/// See also [_preferenceSettingGroup].
class _PreferenceSettingGroupProvider
extends FutureProvider<PreferenceSettingGroup> {
/// 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<PreferenceSettingGroup> 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<PreferenceSettingGroup> 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<PreferenceSettingGroup> {
/// The parameter `groupName` of this provider.
String get groupName;
}
class _PreferenceSettingGroupProviderElement
extends FutureProviderElement<PreferenceSettingGroup>
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<Stream<Map<String, Object>>>>.internal(
_PreferenceRepository.new,
name: r'_preferenceRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$preferenceRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$PreferenceRepository
= AutoDisposeNotifier<Raw<Stream<Map<String, Object>>>>;
String _$preferenceSettingsGeneralRepositoryHash() =>
r'24a70de8284107bb43778e1014abe41bd805987a';
/// See also [PreferenceSettingsGeneralRepository].
@ProviderFor(PreferenceSettingsGeneralRepository)
final preferenceSettingsGeneralRepositoryProvider =
AutoDisposeStreamNotifierProvider<PreferenceSettingsGeneralRepository,
Map<String, PreferenceSettingGroup>>.internal(
PreferenceSettingsGeneralRepository.new,
name: r'preferenceSettingsGeneralRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$preferenceSettingsGeneralRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$PreferenceSettingsGeneralRepository
= AutoDisposeStreamNotifier<Map<String, PreferenceSettingGroup>>;
String _$preferenceSettingsGroupRepositoryHash() =>
r'023794294c5639ac6c6941f87a4ed91e1551fbc9';
abstract class _$PreferenceSettingsGroupRepository
extends BuildlessAutoDisposeStreamNotifier<PreferenceSettingGroup> {
late final String groupName;
Stream<PreferenceSettingGroup> build(
String groupName,
);
}
/// See also [PreferenceSettingsGroupRepository].
@ProviderFor(PreferenceSettingsGroupRepository)
const preferenceSettingsGroupRepositoryProvider =
PreferenceSettingsGroupRepositoryFamily();
/// See also [PreferenceSettingsGroupRepository].
class PreferenceSettingsGroupRepositoryFamily
extends Family<AsyncValue<PreferenceSettingGroup>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? 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<PreferenceSettingGroup> 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<PreferenceSettingsGroupRepository,
PreferenceSettingGroup> 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<PreferenceSettingGroup> {
/// 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
@@ -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';
@@ -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<String?>(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<TabDragData>(
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,
@@ -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';
@@ -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(),
),
);
}
}
@@ -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(),
),
);
}
}
@@ -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),
@@ -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,
);
}
}
@@ -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,
);
}
@@ -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<String, dynamic> 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<String, dynamic> _$EngineSettingsToJson(EngineSettings instance) =>
<String, dynamic>{
'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',
};