pretty good

This commit is contained in:
Fabian Freund
2025-02-07 20:45:47 +01:00
parent 025ecaf2e3
commit a12ef8244f
68 changed files with 2359 additions and 1015 deletions
+1 -1
View File
@@ -97,7 +97,7 @@
</activity>
<activity
android:theme="@style/Theme.AppCompat.Light"
android:theme="@style/AddonsActivityTheme"
android:name="eu.lensai.flutter_mozilla_components.addons.AddonsActivity"
android:label="@string/mozac_feature_addons_addons"
android:exported="false"
@@ -25,4 +25,8 @@
<item name="android:background">?android:attr/colorBackground</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="AddonsActivityTheme" parent="Theme.AppCompat.Light">
<item name="mozac_primary_text_color">@color/photonDarkGrey90</item>
</style>
</resources>
+1 -1
View File
@@ -14,7 +14,7 @@ import 'package:lensai/features/geckoview/features/browser/presentation/screens/
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_edit.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/geckoview/features/search/presentation/screens/search.dart';
import 'package:lensai/features/settings/presentation/screens/bang_settings.dart';
import 'package:lensai/features/settings/presentation/screens/general_settings.dart';
import 'package:lensai/features/settings/presentation/screens/settings.dart';
@@ -274,6 +274,18 @@ class GenericWebsiteService extends _$GenericWebsiteService {
);
}
Future<BrowserIcon> getUrlIcon(Uri url) async {
final cachedIcon = await getCachedIcon(url);
if (cachedIcon != null) {
return cachedIcon;
}
return fetchPageInfo(url).then(
(result) => result.flatMap((pageInfo) => pageInfo.favicon!).value,
);
}
// Future<Uri?> tryUpgradeToHttps(Uri httpUri) async {
// if (httpUri.isScheme('https')) {
// return httpUri;
@@ -7,7 +7,7 @@ part of 'generic_website.dart';
// **************************************************************************
String _$genericWebsiteServiceHash() =>
r'27430ba17a04b0fa3cca20abf00f9d1c15fa843a';
r'4c8c1c9ad430ab01f637f02ec97ee4ce5202975e';
/// See also [GenericWebsiteService].
@ProviderFor(GenericWebsiteService)
@@ -57,17 +57,6 @@ Stream<List<SearchHistoryEntry>> searchHistory(Ref ref) {
return repository.watchSearchHistory(limit: 3); //TODO: make count dynamic
}
@Riverpod()
Future<BangData> bangDataEnsureIcon(
Ref ref,
BangData bang,
) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.ensureIconAvailable(bang).then(
(value) => value.value,
);
}
@Riverpod()
Stream<DateTime?> lastSyncOfGroup(
Ref ref,
@@ -387,138 +387,6 @@ final searchHistoryProvider =
// ignore: unused_element
typedef SearchHistoryRef
= AutoDisposeStreamProviderRef<List<SearchHistoryEntry>>;
String _$bangDataEnsureIconHash() =>
r'423b8e92fd89ec328c2fcc8f9e55365c30633fd2';
/// See also [bangDataEnsureIcon].
@ProviderFor(bangDataEnsureIcon)
const bangDataEnsureIconProvider = BangDataEnsureIconFamily();
/// See also [bangDataEnsureIcon].
class BangDataEnsureIconFamily extends Family<AsyncValue<BangData>> {
/// See also [bangDataEnsureIcon].
const BangDataEnsureIconFamily();
/// See also [bangDataEnsureIcon].
BangDataEnsureIconProvider call(
BangData bang,
) {
return BangDataEnsureIconProvider(
bang,
);
}
@override
BangDataEnsureIconProvider getProviderOverride(
covariant BangDataEnsureIconProvider provider,
) {
return call(
provider.bang,
);
}
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'bangDataEnsureIconProvider';
}
/// See also [bangDataEnsureIcon].
class BangDataEnsureIconProvider extends AutoDisposeFutureProvider<BangData> {
/// See also [bangDataEnsureIcon].
BangDataEnsureIconProvider(
BangData bang,
) : this._internal(
(ref) => bangDataEnsureIcon(
ref as BangDataEnsureIconRef,
bang,
),
from: bangDataEnsureIconProvider,
name: r'bangDataEnsureIconProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$bangDataEnsureIconHash,
dependencies: BangDataEnsureIconFamily._dependencies,
allTransitiveDependencies:
BangDataEnsureIconFamily._allTransitiveDependencies,
bang: bang,
);
BangDataEnsureIconProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.bang,
}) : super.internal();
final BangData bang;
@override
Override overrideWith(
FutureOr<BangData> Function(BangDataEnsureIconRef provider) create,
) {
return ProviderOverride(
origin: this,
override: BangDataEnsureIconProvider._internal(
(ref) => create(ref as BangDataEnsureIconRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
bang: bang,
),
);
}
@override
AutoDisposeFutureProviderElement<BangData> createElement() {
return _BangDataEnsureIconProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is BangDataEnsureIconProvider && other.bang == bang;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, bang.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin BangDataEnsureIconRef on AutoDisposeFutureProviderRef<BangData> {
/// The parameter `bang` of this provider.
BangData get bang;
}
class _BangDataEnsureIconProviderElement
extends AutoDisposeFutureProviderElement<BangData>
with BangDataEnsureIconRef {
_BangDataEnsureIconProviderElement(super.provider);
@override
BangData get bang => (origin as BangDataEnsureIconProvider).bang;
}
String _$lastSyncOfGroupHash() => r'23d07f3132ba9bb35a31f74e3a69698d31d4c569';
/// See also [lastSyncOfGroup].
@@ -111,26 +111,6 @@ class BangDataRepository extends _$BangDataRepository {
.removeSearchEntry(searchQuery);
}
Future<Result<BangData>> ensureIconAvailable(BangData bang) async {
if (bang.icon != null) {
return Result.success(bang);
}
final url = bang.getUrl('');
final websiteProvider = ref.read(genericWebsiteServiceProvider.notifier);
final cachedIcon = await websiteProvider.getCachedIcon(url);
if (cachedIcon != null) {
return Result.success(bang.copyWith.icon(cachedIcon));
}
return websiteProvider.fetchPageInfo(url).then(
(result) => result
.flatMap((pageInfo) => bang.copyWith.icon(pageInfo.favicon)),
);
}
Future<int> resetFrequencies() {
return ref.read(bangDatabaseProvider).bangFrequency.deleteAll();
}
@@ -7,7 +7,7 @@ part of 'data.dart';
// **************************************************************************
String _$bangDataRepositoryHash() =>
r'2c009519cf02df4d3cbb6768dbe55e9eb696e126';
r'de89d7089ce6a92015bee233f08d920981518793';
/// See also [BangDataRepository].
@ProviderFor(BangDataRepository)
@@ -13,12 +13,13 @@ class BangSearch extends _$BangSearch {
Future<void> search(String input) async {
if (input.isNotEmpty) {
await ref
.read(bangDatabaseProvider)
.bangDao
.queryBangs(input)
.get()
.then(_streamController.add);
await ref.read(bangDatabaseProvider).bangDao.queryBangs(input).get().then(
(value) {
if (!_streamController.isClosed) {
_streamController.add(value);
}
},
);
}
}
@@ -6,7 +6,7 @@ part of 'search.dart';
// RiverpodGenerator
// **************************************************************************
String _$bangSearchHash() => r'632ddd09a1198394884cb5c27a2bde7850c0515b';
String _$bangSearchHash() => r'abf9ff5badf2ecfbcbde148fd2c849beac83bec4';
/// See also [BangSearch].
@ProviderFor(BangSearch)
@@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:lensai/presentation/widgets/bang_icon.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
class BangDetails extends HookConsumerWidget {
@@ -38,7 +38,7 @@ class BangDetails extends HookConsumerWidget {
children: [
Row(
children: [
BangIcon(bangData),
UrlIcon(bangData.getUrl(''), iconSize: 34.0),
const SizedBox(width: 12.0),
Expanded(
child: Column(
@@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:skeletonizer/skeletonizer.dart';
class BangIcon extends HookConsumerWidget {
final double iconSize;
final BangData bangData;
const BangIcon(this.bangData, {this.iconSize = 34.0, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bangAsync = ref.watch(bangDataEnsureIconProvider(bangData));
final bang = bangAsync.valueOrNull ?? bangData;
return Skeletonizer(
enabled: bangAsync.isLoading,
child: SizedBox.square(
dimension: iconSize,
child: (bang.icon != null)
? RawImage(
image: bang.icon?.image.value,
height: iconSize,
width: iconSize,
fit: BoxFit.fill,
)
: Icon(
MdiIcons.web,
size: iconSize,
),
),
);
}
}
@@ -5,13 +5,13 @@ import 'package:go_router/go_router.dart';
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/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
import 'package:lensai/features/search/presentation/widgets/search_field.dart';
import 'package:lensai/features/search/presentation/widgets/search_suggestion_list.dart';
import 'package:lensai/features/geckoview/features/search/domain/providers/search_suggestions.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_field.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/search_suggestions.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/bang_icon.dart';
import 'package:lensai/presentation/widgets/selectable_chips.dart';
class SiteSearch extends HookConsumerWidget {
@@ -70,7 +70,7 @@ class SiteSearch extends HookConsumerWidget {
width: double.maxFinite,
child: SelectableChips(
itemId: (bang) => bang.trigger,
itemAvatar: (bang) => BangIcon(bang, iconSize: 20),
itemAvatar: (bang) => UrlIcon(bang.getUrl(''), iconSize: 20),
itemLabel: (bang) => Text(bang.websiteName),
availableItems: availableBangs,
selectedItem: selectedBang,
@@ -98,9 +98,10 @@ class SiteSearch extends HookConsumerWidget {
SearchField(
textEditingController: searchTextController,
activeBang: activeBang,
onFieldSubmitted: (_) async {
onSubmitted: (_) async {
await submitSearch(searchTextController.text);
},
showSuggestions: false,
),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150),
@@ -111,10 +112,12 @@ class SiteSearch extends HookConsumerWidget {
shrinkWrap: true,
controller: controller,
slivers: [
SearchSuggestionList(
SearchTermSuggestions(
searchTextController: searchTextController,
activeBang: activeBang,
submitSearch: submitSearch,
showHistory: false,
showChips: false,
),
],
);
@@ -74,7 +74,13 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
ellipsis: ellipsis,
)
.get()
.then(_streamController.add);
.then(
(value) {
if (!_streamController.isClosed) {
_streamController.add(value);
}
},
);
}
}
@@ -7,7 +7,7 @@ part of 'search.dart';
// **************************************************************************
String _$chatArchiveSearchRepositoryHash() =>
r'6762958d43d2efc95f5d9848bfcae41436564f2b';
r'407023f1ee8683f21d4263dccc9eab1d7a40278e';
/// See also [ChatArchiveSearchRepository].
@ProviderFor(ChatArchiveSearchRepository)
@@ -29,7 +29,9 @@ class BottomSheetExtend extends _$BottomSheetExtend {
late StreamController<double> _extentStreamController;
void add(double extent) {
_extentStreamController.add(extent);
if (!_extentStreamController.isClosed) {
_extentStreamController.add(extent);
}
}
@override
@@ -23,7 +23,7 @@ final bottomSheetControllerProvider =
);
typedef _$BottomSheetController = AutoDisposeNotifier<Sheet?>;
String _$bottomSheetExtendHash() => r'81ae254499d1d7f57309274b0f18747b94d6dda3';
String _$bottomSheetExtendHash() => r'7e11b9047c15bdeb4dfcb488a8573daadb64e25c';
/// See also [BottomSheetExtend].
@ProviderFor(BottomSheetExtend)
@@ -107,6 +107,17 @@ GeckoTabContentService tabContentService(Ref ref) {
return service;
}
@Riverpod(keepAlive: true)
GeckoSuggestionsService engineSuggestionsService(Ref ref) {
final service = GeckoSuggestionsService.setUp();
ref.onDispose(() {
service.dispose();
});
return service;
}
@Riverpod(keepAlive: true)
class EngineReadyState extends _$EngineReadyState {
@override
@@ -74,6 +74,25 @@ final tabContentServiceProvider = Provider<GeckoTabContentService>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef TabContentServiceRef = ProviderRef<GeckoTabContentService>;
String _$engineSuggestionsServiceHash() =>
r'f7414b335564578b2c7f6a86baf8bf13f2d5ba2d';
/// See also [engineSuggestionsService].
@ProviderFor(engineSuggestionsService)
final engineSuggestionsServiceProvider =
Provider<GeckoSuggestionsService>.internal(
engineSuggestionsService,
name: r'engineSuggestionsServiceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$engineSuggestionsServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef EngineSuggestionsServiceRef = ProviderRef<GeckoSuggestionsService>;
String _$selectedTabSessionNotifierHash() =>
r'aef19991d5b05a2bb28e8a6ccb57dc75c2f3148f';
@@ -3,9 +3,14 @@
import 'dart:async';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart';
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/features/search/domain/entities/tab_preview.dart';
import 'package:lensai/features/geckoview/features/tabs/data/entities/container_filter.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
@@ -87,27 +92,48 @@ class ShowFindInPage extends _$ShowFindInPage {
}
@Riverpod()
List<String> availableTabIds(
EquatableCollection<List<String>> availableTabIds(
Ref ref,
String? containerId,
ContainerFilter containerFilter,
) {
final containerTabs = ref.watch(
containerTabIdsProvider(containerId).select((value) => value.valueOrNull),
containerTabIdsProvider(containerFilter)
.select((value) => value.valueOrNull),
);
final tabStates = ref.watch(tabListProvider);
final tabList = ref.watch(tabListProvider);
return containerTabs?.where((tabId) => tabStates.contains(tabId)).toList() ??
[];
return EquatableCollection(
containerTabs?.where((tabId) => tabList.contains(tabId)).toList() ?? [],
immutable: true,
);
}
@Riverpod()
List<String> seamlessFilteredTabs(
EquatableCollection<Map<String, TabState>> availableTabStates(
Ref ref,
String? containerId,
ContainerFilter containerFilter,
) {
final availableTabs = ref.watch(availableTabIdsProvider(containerFilter));
final tabStates = ref.watch(tabStatesProvider);
return EquatableCollection(
{
for (final tabId in availableTabs.collection)
if (tabStates.containsKey(tabId)) tabId: tabStates[tabId]!,
},
immutable: true,
);
}
@Riverpod()
EquatableCollection<List<String>> seamlessFilteredTabIds(
Ref ref,
TabSearchPartition searchPartition,
ContainerFilter containerFilter,
) {
final tabSearchResults = ref
.watch(
tabSearchRepositoryProvider.select(
tabSearchRepositoryProvider(searchPartition).select(
(value) => EquatableCollection(
value.valueOrNull?.map((tab) => tab.id).toList(),
immutable: true,
@@ -116,17 +142,74 @@ List<String> seamlessFilteredTabs(
)
.collection;
final availableTabs = ref
.watch(
availableTabIdsProvider(containerId).select(
(value) => EquatableCollection(value, immutable: true),
),
)
.collection;
final availableTabs = ref.watch(availableTabIdsProvider(containerFilter));
if (tabSearchResults == null) {
return availableTabs;
}
return tabSearchResults.where((tab) => availableTabs.contains(tab)).toList();
return EquatableCollection(
tabSearchResults
.where((tab) => availableTabs.collection.contains(tab))
.toList(),
immutable: true,
);
}
@Riverpod()
EquatableCollection<List<TabPreview>> seamlessFilteredTabPreviews(
Ref ref,
TabSearchPartition searchPartition,
ContainerFilter containerFilter,
) {
final tabSearchResults = ref
.watch(
tabSearchRepositoryProvider(searchPartition).select(
(value) => EquatableCollection(
value.valueOrNull,
immutable: true,
),
),
)
.collection;
final availableTabStates =
ref.watch(availableTabStatesProvider(containerFilter));
if (tabSearchResults == null) {
return EquatableCollection(
availableTabStates.collection.values
.map(
(state) => TabPreview(
id: state.id,
title: state.title,
icon: state.icon,
url: state.url,
highlightedUrl: null,
content: null,
),
)
.toList(),
immutable: true,
);
}
return EquatableCollection(
tabSearchResults
.where((tab) => availableTabStates.collection.containsKey(tab.id))
.map((tab) {
return TabPreview(
id: tab.id,
title: tab.title ?? availableTabStates.collection[tab.id]!.title,
icon: null,
url: tab.cleanUrl.mapNotNull(Uri.tryParse) ??
availableTabStates.collection[tab.id]!.url,
highlightedUrl: tab.url,
content: tab.extractedContent ?? tab.fullContent,
);
})
.whereType<TabPreview>()
.toList(),
immutable: true,
);
}
@@ -158,23 +158,23 @@ class _SelectedBangDataProviderElement
String? get domain => (origin as SelectedBangDataProvider).domain;
}
String _$availableTabIdsHash() => r'646d42c747d815496849c7342903caf48258a8ab';
String _$availableTabIdsHash() => r'd8b55b012b38111c8d5e069987a55645d1a71b89';
/// See also [availableTabIds].
@ProviderFor(availableTabIds)
const availableTabIdsProvider = AvailableTabIdsFamily();
/// See also [availableTabIds].
class AvailableTabIdsFamily extends Family<List<String>> {
class AvailableTabIdsFamily extends Family<EquatableCollection<List<String>>> {
/// See also [availableTabIds].
const AvailableTabIdsFamily();
/// See also [availableTabIds].
AvailableTabIdsProvider call(
String? containerId,
ContainerFilter containerFilter,
) {
return AvailableTabIdsProvider(
containerId,
containerFilter,
);
}
@@ -183,7 +183,7 @@ class AvailableTabIdsFamily extends Family<List<String>> {
covariant AvailableTabIdsProvider provider,
) {
return call(
provider.containerId,
provider.containerFilter,
);
}
@@ -203,14 +203,15 @@ class AvailableTabIdsFamily extends Family<List<String>> {
}
/// See also [availableTabIds].
class AvailableTabIdsProvider extends AutoDisposeProvider<List<String>> {
class AvailableTabIdsProvider
extends AutoDisposeProvider<EquatableCollection<List<String>>> {
/// See also [availableTabIds].
AvailableTabIdsProvider(
String? containerId,
ContainerFilter containerFilter,
) : this._internal(
(ref) => availableTabIds(
ref as AvailableTabIdsRef,
containerId,
containerFilter,
),
from: availableTabIdsProvider,
name: r'availableTabIdsProvider',
@@ -221,7 +222,7 @@ class AvailableTabIdsProvider extends AutoDisposeProvider<List<String>> {
dependencies: AvailableTabIdsFamily._dependencies,
allTransitiveDependencies:
AvailableTabIdsFamily._allTransitiveDependencies,
containerId: containerId,
containerFilter: containerFilter,
);
AvailableTabIdsProvider._internal(
@@ -231,14 +232,15 @@ class AvailableTabIdsProvider extends AutoDisposeProvider<List<String>> {
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.containerId,
required this.containerFilter,
}) : super.internal();
final String? containerId;
final ContainerFilter containerFilter;
@override
Override overrideWith(
List<String> Function(AvailableTabIdsRef provider) create,
EquatableCollection<List<String>> Function(AvailableTabIdsRef provider)
create,
) {
return ProviderOverride(
origin: this,
@@ -249,25 +251,27 @@ class AvailableTabIdsProvider extends AutoDisposeProvider<List<String>> {
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
containerId: containerId,
containerFilter: containerFilter,
),
);
}
@override
AutoDisposeProviderElement<List<String>> createElement() {
AutoDisposeProviderElement<EquatableCollection<List<String>>>
createElement() {
return _AvailableTabIdsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AvailableTabIdsProvider && other.containerId == containerId;
return other is AvailableTabIdsProvider &&
other.containerFilter == containerFilter;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, containerId.hashCode);
hash = _SystemHash.combine(hash, containerFilter.hashCode);
return _SystemHash.finish(hash);
}
@@ -275,46 +279,50 @@ class AvailableTabIdsProvider extends AutoDisposeProvider<List<String>> {
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AvailableTabIdsRef on AutoDisposeProviderRef<List<String>> {
/// The parameter `containerId` of this provider.
String? get containerId;
mixin AvailableTabIdsRef
on AutoDisposeProviderRef<EquatableCollection<List<String>>> {
/// The parameter `containerFilter` of this provider.
ContainerFilter get containerFilter;
}
class _AvailableTabIdsProviderElement
extends AutoDisposeProviderElement<List<String>> with AvailableTabIdsRef {
extends AutoDisposeProviderElement<EquatableCollection<List<String>>>
with AvailableTabIdsRef {
_AvailableTabIdsProviderElement(super.provider);
@override
String? get containerId => (origin as AvailableTabIdsProvider).containerId;
ContainerFilter get containerFilter =>
(origin as AvailableTabIdsProvider).containerFilter;
}
String _$seamlessFilteredTabsHash() =>
r'1abfb0e9ae834cdde5ab64b6174f24f45f36a272';
String _$availableTabStatesHash() =>
r'ca42dc8cb88070fb58ab085ab6c9bcbf31ef4f0b';
/// See also [seamlessFilteredTabs].
@ProviderFor(seamlessFilteredTabs)
const seamlessFilteredTabsProvider = SeamlessFilteredTabsFamily();
/// See also [availableTabStates].
@ProviderFor(availableTabStates)
const availableTabStatesProvider = AvailableTabStatesFamily();
/// See also [seamlessFilteredTabs].
class SeamlessFilteredTabsFamily extends Family<List<String>> {
/// See also [seamlessFilteredTabs].
const SeamlessFilteredTabsFamily();
/// See also [availableTabStates].
class AvailableTabStatesFamily
extends Family<EquatableCollection<Map<String, TabState>>> {
/// See also [availableTabStates].
const AvailableTabStatesFamily();
/// See also [seamlessFilteredTabs].
SeamlessFilteredTabsProvider call(
String? containerId,
/// See also [availableTabStates].
AvailableTabStatesProvider call(
ContainerFilter containerFilter,
) {
return SeamlessFilteredTabsProvider(
containerId,
return AvailableTabStatesProvider(
containerFilter,
);
}
@override
SeamlessFilteredTabsProvider getProviderOverride(
covariant SeamlessFilteredTabsProvider provider,
AvailableTabStatesProvider getProviderOverride(
covariant AvailableTabStatesProvider provider,
) {
return call(
provider.containerId,
provider.containerFilter,
);
}
@@ -330,76 +338,80 @@ class SeamlessFilteredTabsFamily extends Family<List<String>> {
_allTransitiveDependencies;
@override
String? get name => r'seamlessFilteredTabsProvider';
String? get name => r'availableTabStatesProvider';
}
/// See also [seamlessFilteredTabs].
class SeamlessFilteredTabsProvider extends AutoDisposeProvider<List<String>> {
/// See also [seamlessFilteredTabs].
SeamlessFilteredTabsProvider(
String? containerId,
/// See also [availableTabStates].
class AvailableTabStatesProvider
extends AutoDisposeProvider<EquatableCollection<Map<String, TabState>>> {
/// See also [availableTabStates].
AvailableTabStatesProvider(
ContainerFilter containerFilter,
) : this._internal(
(ref) => seamlessFilteredTabs(
ref as SeamlessFilteredTabsRef,
containerId,
(ref) => availableTabStates(
ref as AvailableTabStatesRef,
containerFilter,
),
from: seamlessFilteredTabsProvider,
name: r'seamlessFilteredTabsProvider',
from: availableTabStatesProvider,
name: r'availableTabStatesProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$seamlessFilteredTabsHash,
dependencies: SeamlessFilteredTabsFamily._dependencies,
: _$availableTabStatesHash,
dependencies: AvailableTabStatesFamily._dependencies,
allTransitiveDependencies:
SeamlessFilteredTabsFamily._allTransitiveDependencies,
containerId: containerId,
AvailableTabStatesFamily._allTransitiveDependencies,
containerFilter: containerFilter,
);
SeamlessFilteredTabsProvider._internal(
AvailableTabStatesProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.containerId,
required this.containerFilter,
}) : super.internal();
final String? containerId;
final ContainerFilter containerFilter;
@override
Override overrideWith(
List<String> Function(SeamlessFilteredTabsRef provider) create,
EquatableCollection<Map<String, TabState>> Function(
AvailableTabStatesRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: SeamlessFilteredTabsProvider._internal(
(ref) => create(ref as SeamlessFilteredTabsRef),
override: AvailableTabStatesProvider._internal(
(ref) => create(ref as AvailableTabStatesRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
containerId: containerId,
containerFilter: containerFilter,
),
);
}
@override
AutoDisposeProviderElement<List<String>> createElement() {
return _SeamlessFilteredTabsProviderElement(this);
AutoDisposeProviderElement<EquatableCollection<Map<String, TabState>>>
createElement() {
return _AvailableTabStatesProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SeamlessFilteredTabsProvider &&
other.containerId == containerId;
return other is AvailableTabStatesProvider &&
other.containerFilter == containerFilter;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, containerId.hashCode);
hash = _SystemHash.combine(hash, containerFilter.hashCode);
return _SystemHash.finish(hash);
}
@@ -407,19 +419,333 @@ class SeamlessFilteredTabsProvider extends AutoDisposeProvider<List<String>> {
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SeamlessFilteredTabsRef on AutoDisposeProviderRef<List<String>> {
/// The parameter `containerId` of this provider.
String? get containerId;
mixin AvailableTabStatesRef
on AutoDisposeProviderRef<EquatableCollection<Map<String, TabState>>> {
/// The parameter `containerFilter` of this provider.
ContainerFilter get containerFilter;
}
class _SeamlessFilteredTabsProviderElement
extends AutoDisposeProviderElement<List<String>>
with SeamlessFilteredTabsRef {
_SeamlessFilteredTabsProviderElement(super.provider);
class _AvailableTabStatesProviderElement extends AutoDisposeProviderElement<
EquatableCollection<Map<String, TabState>>> with AvailableTabStatesRef {
_AvailableTabStatesProviderElement(super.provider);
@override
String? get containerId =>
(origin as SeamlessFilteredTabsProvider).containerId;
ContainerFilter get containerFilter =>
(origin as AvailableTabStatesProvider).containerFilter;
}
String _$seamlessFilteredTabIdsHash() =>
r'2e29ca53c35be89972d3f9aea24198fe1feb9e05';
/// See also [seamlessFilteredTabIds].
@ProviderFor(seamlessFilteredTabIds)
const seamlessFilteredTabIdsProvider = SeamlessFilteredTabIdsFamily();
/// See also [seamlessFilteredTabIds].
class SeamlessFilteredTabIdsFamily
extends Family<EquatableCollection<List<String>>> {
/// See also [seamlessFilteredTabIds].
const SeamlessFilteredTabIdsFamily();
/// See also [seamlessFilteredTabIds].
SeamlessFilteredTabIdsProvider call(
TabSearchPartition searchPartition,
ContainerFilter containerFilter,
) {
return SeamlessFilteredTabIdsProvider(
searchPartition,
containerFilter,
);
}
@override
SeamlessFilteredTabIdsProvider getProviderOverride(
covariant SeamlessFilteredTabIdsProvider provider,
) {
return call(
provider.searchPartition,
provider.containerFilter,
);
}
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'seamlessFilteredTabIdsProvider';
}
/// See also [seamlessFilteredTabIds].
class SeamlessFilteredTabIdsProvider
extends AutoDisposeProvider<EquatableCollection<List<String>>> {
/// See also [seamlessFilteredTabIds].
SeamlessFilteredTabIdsProvider(
TabSearchPartition searchPartition,
ContainerFilter containerFilter,
) : this._internal(
(ref) => seamlessFilteredTabIds(
ref as SeamlessFilteredTabIdsRef,
searchPartition,
containerFilter,
),
from: seamlessFilteredTabIdsProvider,
name: r'seamlessFilteredTabIdsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$seamlessFilteredTabIdsHash,
dependencies: SeamlessFilteredTabIdsFamily._dependencies,
allTransitiveDependencies:
SeamlessFilteredTabIdsFamily._allTransitiveDependencies,
searchPartition: searchPartition,
containerFilter: containerFilter,
);
SeamlessFilteredTabIdsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.searchPartition,
required this.containerFilter,
}) : super.internal();
final TabSearchPartition searchPartition;
final ContainerFilter containerFilter;
@override
Override overrideWith(
EquatableCollection<List<String>> Function(
SeamlessFilteredTabIdsRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: SeamlessFilteredTabIdsProvider._internal(
(ref) => create(ref as SeamlessFilteredTabIdsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
searchPartition: searchPartition,
containerFilter: containerFilter,
),
);
}
@override
AutoDisposeProviderElement<EquatableCollection<List<String>>>
createElement() {
return _SeamlessFilteredTabIdsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SeamlessFilteredTabIdsProvider &&
other.searchPartition == searchPartition &&
other.containerFilter == containerFilter;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, searchPartition.hashCode);
hash = _SystemHash.combine(hash, containerFilter.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SeamlessFilteredTabIdsRef
on AutoDisposeProviderRef<EquatableCollection<List<String>>> {
/// The parameter `searchPartition` of this provider.
TabSearchPartition get searchPartition;
/// The parameter `containerFilter` of this provider.
ContainerFilter get containerFilter;
}
class _SeamlessFilteredTabIdsProviderElement
extends AutoDisposeProviderElement<EquatableCollection<List<String>>>
with SeamlessFilteredTabIdsRef {
_SeamlessFilteredTabIdsProviderElement(super.provider);
@override
TabSearchPartition get searchPartition =>
(origin as SeamlessFilteredTabIdsProvider).searchPartition;
@override
ContainerFilter get containerFilter =>
(origin as SeamlessFilteredTabIdsProvider).containerFilter;
}
String _$seamlessFilteredTabPreviewsHash() =>
r'7a70eb2f3f59de5188d94a73790e39c339a6293b';
/// See also [seamlessFilteredTabPreviews].
@ProviderFor(seamlessFilteredTabPreviews)
const seamlessFilteredTabPreviewsProvider = SeamlessFilteredTabPreviewsFamily();
/// See also [seamlessFilteredTabPreviews].
class SeamlessFilteredTabPreviewsFamily
extends Family<EquatableCollection<List<TabPreview>>> {
/// See also [seamlessFilteredTabPreviews].
const SeamlessFilteredTabPreviewsFamily();
/// See also [seamlessFilteredTabPreviews].
SeamlessFilteredTabPreviewsProvider call(
TabSearchPartition searchPartition,
ContainerFilter containerFilter,
) {
return SeamlessFilteredTabPreviewsProvider(
searchPartition,
containerFilter,
);
}
@override
SeamlessFilteredTabPreviewsProvider getProviderOverride(
covariant SeamlessFilteredTabPreviewsProvider provider,
) {
return call(
provider.searchPartition,
provider.containerFilter,
);
}
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'seamlessFilteredTabPreviewsProvider';
}
/// See also [seamlessFilteredTabPreviews].
class SeamlessFilteredTabPreviewsProvider
extends AutoDisposeProvider<EquatableCollection<List<TabPreview>>> {
/// See also [seamlessFilteredTabPreviews].
SeamlessFilteredTabPreviewsProvider(
TabSearchPartition searchPartition,
ContainerFilter containerFilter,
) : this._internal(
(ref) => seamlessFilteredTabPreviews(
ref as SeamlessFilteredTabPreviewsRef,
searchPartition,
containerFilter,
),
from: seamlessFilteredTabPreviewsProvider,
name: r'seamlessFilteredTabPreviewsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$seamlessFilteredTabPreviewsHash,
dependencies: SeamlessFilteredTabPreviewsFamily._dependencies,
allTransitiveDependencies:
SeamlessFilteredTabPreviewsFamily._allTransitiveDependencies,
searchPartition: searchPartition,
containerFilter: containerFilter,
);
SeamlessFilteredTabPreviewsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.searchPartition,
required this.containerFilter,
}) : super.internal();
final TabSearchPartition searchPartition;
final ContainerFilter containerFilter;
@override
Override overrideWith(
EquatableCollection<List<TabPreview>> Function(
SeamlessFilteredTabPreviewsRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: SeamlessFilteredTabPreviewsProvider._internal(
(ref) => create(ref as SeamlessFilteredTabPreviewsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
searchPartition: searchPartition,
containerFilter: containerFilter,
),
);
}
@override
AutoDisposeProviderElement<EquatableCollection<List<TabPreview>>>
createElement() {
return _SeamlessFilteredTabPreviewsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SeamlessFilteredTabPreviewsProvider &&
other.searchPartition == searchPartition &&
other.containerFilter == containerFilter;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, searchPartition.hashCode);
hash = _SystemHash.combine(hash, containerFilter.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SeamlessFilteredTabPreviewsRef
on AutoDisposeProviderRef<EquatableCollection<List<TabPreview>>> {
/// The parameter `searchPartition` of this provider.
TabSearchPartition get searchPartition;
/// The parameter `containerFilter` of this provider.
ContainerFilter get containerFilter;
}
class _SeamlessFilteredTabPreviewsProviderElement
extends AutoDisposeProviderElement<EquatableCollection<List<TabPreview>>>
with SeamlessFilteredTabPreviewsRef {
_SeamlessFilteredTabPreviewsProviderElement(super.provider);
@override
TabSearchPartition get searchPartition =>
(origin as SeamlessFilteredTabPreviewsProvider).searchPartition;
@override
ContainerFilter get containerFilter =>
(origin as SeamlessFilteredTabPreviewsProvider).containerFilter;
}
String _$selectedBangTriggerHash() =>
@@ -48,6 +48,8 @@ class CreateTabStream extends _$CreateTabStream {
}
void createTab(CreateTabSheet parameter) {
_streamController.add(parameter);
if (!_streamController.isClosed) {
_streamController.add(parameter);
}
}
}
@@ -6,7 +6,7 @@ part of 'create_tab.dart';
// RiverpodGenerator
// **************************************************************************
String _$createTabStreamHash() => r'05ad914e41e0d346e082e4638afe5fe5a04052f1';
String _$createTabStreamHash() => r'2e802a10ecaf1862074f32b76e89c26997bdd7b2';
/// See also [CreateTabStream].
@ProviderFor(CreateTabStream)
@@ -7,15 +7,13 @@ 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/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
import 'package:lensai/features/geckoview/features/tabs/data/entities/container_filter.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';
@@ -94,7 +92,9 @@ class _Tab extends HookConsumerWidget {
searchTextController,
() async {
await ref
.read(tabSearchRepositoryProvider.notifier)
.read(
tabSearchRepositoryProvider(TabSearchPartition.preview).notifier,
)
.addQuery(searchTextController.text);
},
);
@@ -248,14 +248,12 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
builder: (context, ref, child) {
final container = ref.watch(selectedContainerProvider);
final filteredTabs = ref
.watch(
seamlessFilteredTabsProvider(container).select(
(value) =>
EquatableCollection(value, immutable: true),
),
)
.collection;
final filteredTabIds = ref.watch(
seamlessFilteredTabIdsProvider(
TabSearchPartition.preview,
ContainerFilterById(containerId: container),
),
);
final activeTab = ref.watch(selectedTabProvider);
@@ -273,7 +271,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
useEffect(
() {
final index = filteredTabs
final index = filteredTabIds.collection
.indexWhere((webView) => webView == activeTab);
if (index > -1) {
@@ -292,12 +290,12 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
return null;
},
[filteredTabs, activeTab],
[filteredTabIds, activeTab],
);
final tabs = useMemoized(
() {
return filteredTabs
return filteredTabIds.collection
.mapIndexed(
(index, tabId) => CustomDraggable(
key: Key(tabId),
@@ -350,7 +348,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
.toList();
},
[
EquatableCollection(filteredTabs, immutable: true),
filteredTabIds,
activeTab,
],
);
@@ -375,7 +373,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
final containerRepository =
ref.read(containerRepositoryProvider.notifier);
final tabId = filteredTabs[oldIndex];
final tabId = filteredTabIds.collection[oldIndex];
final containerId = await ref
.read(tabDataRepositoryProvider.notifier)
.containerTabId(tabId);
@@ -384,13 +382,14 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
if (newIndex <= 0) {
key = await containerRepository
.getLeadingOrderKey(containerId);
} else if (newIndex >= filteredTabs.length - 1) {
} else if (newIndex >=
filteredTabIds.collection.length - 1) {
key = await containerRepository
.getTrailingOrderKey(containerId);
} else {
final orderAfterIndex = newIndex;
key = await containerRepository.getOrderKeyAfterTab(
filteredTabs[orderAfterIndex],
filteredTabIds.collection[orderAfterIndex],
containerId,
);
}
@@ -0,0 +1,36 @@
import 'package:fast_equatable/fast_equatable.dart';
import 'package:lensai/domain/entities/equatable_image.dart';
class TabPreview with FastEquatable {
final String id;
final String title;
final EquatableImage? icon;
final Uri url;
final String? highlightedUrl;
final String? content;
TabPreview({
required this.id,
required this.title,
required this.icon,
required this.url,
required this.highlightedUrl,
required this.content,
});
@override
List<Object?> get hashParameters => [
id,
title,
icon,
url,
highlightedUrl,
content,
];
@override
bool get cacheHash => true;
}
@@ -0,0 +1,55 @@
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
part 'engine_suggestions.g.dart';
@Riverpod()
class EngineSuggestions extends _$EngineSuggestions {
Future<String?> getAutocompleteSuggestion(String query) {
return ref
.read(engineSuggestionsServiceProvider)
.getAutocompleteSuggestion(query)
.then((result) => result?.text);
}
Future<void> addQuery(
String query, {
List<GeckoSuggestionType> providers = const [
GeckoSuggestionType.history,
],
}) {
return ref
.read(engineSuggestionsServiceProvider)
.querySuggestions(query, providers: providers);
}
@override
Stream<List<GeckoSuggestion>> build() {
final service = ref.watch(engineSuggestionsServiceProvider);
return ConcatStream([Stream.value([]), service.suggestionsStream]);
}
}
@Riverpod()
AsyncValue<List<GeckoSuggestion>> engineHistorySuggestions(Ref ref) {
return ref.watch(
engineSuggestionsProvider.select(
(suggestions) => suggestions.whenData(
(suggestions) => suggestions
.where(
(suggestion) =>
suggestion.type == GeckoSuggestionType.history &&
(suggestion.title?.isNotEmpty ?? false) &&
(suggestion.description
.mapNotNull((url) => Uri.tryParse(url)) !=
null),
)
.toList(),
),
),
);
}
@@ -0,0 +1,46 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'engine_suggestions.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$engineHistorySuggestionsHash() =>
r'218f1da2140a76c3b02634619955b1382c6ef182';
/// See also [engineHistorySuggestions].
@ProviderFor(engineHistorySuggestions)
final engineHistorySuggestionsProvider =
AutoDisposeProvider<AsyncValue<List<GeckoSuggestion>>>.internal(
engineHistorySuggestions,
name: r'engineHistorySuggestionsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$engineHistorySuggestionsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef EngineHistorySuggestionsRef
= AutoDisposeProviderRef<AsyncValue<List<GeckoSuggestion>>>;
String _$engineSuggestionsHash() => r'ba2c2d7f0a5e99fd9e639acd020b8cf3016c6126';
/// See also [EngineSuggestions].
@ProviderFor(EngineSuggestions)
final engineSuggestionsProvider = AutoDisposeStreamNotifierProvider<
EngineSuggestions, List<GeckoSuggestion>>.internal(
EngineSuggestions.new,
name: r'engineSuggestionsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$engineSuggestionsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$EngineSuggestions = AutoDisposeStreamNotifier<List<GeckoSuggestion>>;
// 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
@@ -11,14 +11,14 @@ ISearchSuggestionProvider defaultSearchSuggestions(Ref ref) {
return ref.watch(kagiAutosuggestServiceProvider.notifier);
}
@Riverpod(keepAlive: true)
@Riverpod()
class SearchSuggestions extends _$SearchSuggestions {
late void Function(String query) _addQueryBinding;
void addQuery(String query) => _addQueryBinding(query);
@override
Raw<Stream<List<String>>> build({
Stream<List<String>> build({
ISearchSuggestionProvider? suggestionsProvider,
}) {
final defaultProvider = ref.watch(defaultSearchSuggestionsProvider);
@@ -25,7 +25,7 @@ final defaultSearchSuggestionsProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef DefaultSearchSuggestionsRef = ProviderRef<ISearchSuggestionProvider>;
String _$searchSuggestionsHash() => r'ecc378aca325cc6759292a06d8d4d5ace910eb7f';
String _$searchSuggestionsHash() => r'8d1fb72cab374491a53f7a814f87af6a26356f42';
/// Copied from Dart SDK
class _SystemHash {
@@ -49,10 +49,10 @@ class _SystemHash {
}
abstract class _$SearchSuggestions
extends BuildlessNotifier<Raw<Stream<List<String>>>> {
extends BuildlessAutoDisposeStreamNotifier<List<String>> {
late final ISearchSuggestionProvider? suggestionsProvider;
Raw<Stream<List<String>>> build({
Stream<List<String>> build({
ISearchSuggestionProvider? suggestionsProvider,
});
}
@@ -62,7 +62,7 @@ abstract class _$SearchSuggestions
const searchSuggestionsProvider = SearchSuggestionsFamily();
/// See also [SearchSuggestions].
class SearchSuggestionsFamily extends Family<Raw<Stream<List<String>>>> {
class SearchSuggestionsFamily extends Family<AsyncValue<List<String>>> {
/// See also [SearchSuggestions].
const SearchSuggestionsFamily();
@@ -100,8 +100,8 @@ class SearchSuggestionsFamily extends Family<Raw<Stream<List<String>>>> {
}
/// See also [SearchSuggestions].
class SearchSuggestionsProvider
extends NotifierProviderImpl<SearchSuggestions, Raw<Stream<List<String>>>> {
class SearchSuggestionsProvider extends AutoDisposeStreamNotifierProviderImpl<
SearchSuggestions, List<String>> {
/// See also [SearchSuggestions].
SearchSuggestionsProvider({
ISearchSuggestionProvider? suggestionsProvider,
@@ -132,7 +132,7 @@ class SearchSuggestionsProvider
final ISearchSuggestionProvider? suggestionsProvider;
@override
Raw<Stream<List<String>>> runNotifierBuild(
Stream<List<String>> runNotifierBuild(
covariant SearchSuggestions notifier,
) {
return notifier.build(
@@ -157,7 +157,7 @@ class SearchSuggestionsProvider
}
@override
NotifierProviderElement<SearchSuggestions, Raw<Stream<List<String>>>>
AutoDisposeStreamNotifierProviderElement<SearchSuggestions, List<String>>
createElement() {
return _SearchSuggestionsProviderElement(this);
}
@@ -179,13 +179,15 @@ class SearchSuggestionsProvider
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SearchSuggestionsRef on NotifierProviderRef<Raw<Stream<List<String>>>> {
mixin SearchSuggestionsRef
on AutoDisposeStreamNotifierProviderRef<List<String>> {
/// The parameter `suggestionsProvider` of this provider.
ISearchSuggestionProvider? get suggestionsProvider;
}
class _SearchSuggestionsProviderElement extends NotifierProviderElement<
SearchSuggestions, Raw<Stream<List<String>>>> with SearchSuggestionsRef {
class _SearchSuggestionsProviderElement
extends AutoDisposeStreamNotifierProviderElement<SearchSuggestions,
List<String>> with SearchSuggestionsRef {
_SearchSuggestionsProviderElement(super.provider);
@override
@@ -0,0 +1,121 @@
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/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/domain/providers/search.dart';
import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.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/search/presentation/widgets/search_field.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/search_suggestions.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart';
class SearchScreen extends HookConsumerWidget {
final String? initialSearchText;
const SearchScreen({required this.initialSearchText});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final searchTextController =
useTextEditingController(text: initialSearchText);
final searchFocusNode = useFocusNode();
final defaultSearchBang = ref.watch(
defaultSearchBangDataProvider.select((value) => value.valueOrNull),
);
final selectedBang = useState<BangData?>(null);
final activeBang = selectedBang.value ?? defaultSearchBang;
final showBangIcon = useState(false);
ref.listen(
selectedBangDataProvider(),
(previous, next) {
if (next.hasValue) {
if ((previous?.hasValue ?? false) && previous!.value != next.value) {
showBangIcon.value = true;
}
selectedBang.value = next.value;
}
},
);
Future<void> submitSearch(String query) async {
if (activeBang != null && (formKey.currentState?.validate() == true)) {
final searchUri = await ref.read(
triggerBangSearchProvider(activeBang, query).future,
);
await ref.read(tabRepositoryProvider.notifier).addTab(url: searchUri);
if (context.mounted) {
ref.read(bottomSheetControllerProvider.notifier).dismiss();
context.pop();
}
}
}
return Scaffold(
body: Form(
key: formKey,
child: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
pinned: true,
automaticallyImplyLeading: false,
title: SearchField(
showBangIcon: showBangIcon.value,
textEditingController: searchTextController,
focusNode: searchFocusNode,
autofocus: true,
onSubmitted: (text) async {
if (Uri.tryParse(text) case final Uri url) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: url);
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.dismiss();
context.pop();
}
}
},
activeBang: activeBang,
showSuggestions: true,
),
),
const SliverToBoxAdapter(
child: Divider(),
),
SearchTermSuggestions(
searchTextController: searchTextController,
activeBang: activeBang,
submitSearch: submitSearch,
),
const SliverToBoxAdapter(
child: Divider(),
),
TabSearch(
searchTextController: searchTextController,
),
HistorySuggestions(
searchTextController: searchTextController,
),
],
),
),
);
}
}
@@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/repositories/search.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:lensai/presentation/widgets/bang_icon.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/selectable_chips.dart';
@@ -75,7 +75,8 @@ class BangChips extends HookConsumerWidget {
Expanded(
child: SelectableChips(
itemId: (bang) => bang.trigger,
itemAvatar: (bang) => BangIcon(bang, iconSize: 20),
itemAvatar: (bang) =>
UrlIcon(bang.getUrl(''), iconSize: 20),
itemLabel: (bang) => Text(bang.websiteName),
availableItems: availableBangs,
selectedItem: activeBang,
@@ -2,14 +2,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:lensai/features/geckoview/features/search/domain/providers/engine_suggestions.dart';
import 'package:lensai/features/user/domain/providers.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/auto_suggest_text_field.dart';
import 'package:lensai/presentation/widgets/bang_icon.dart';
import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
class SearchField extends HookConsumerWidget {
final TextEditingController textEditingController;
final FocusNode? focusNode;
final void Function(String)? onFieldSubmitted;
final bool autofocus;
final void Function(String)? onSubmitted;
final bool showSuggestions;
final BangData? activeBang;
final bool showBangIcon;
@@ -17,10 +22,12 @@ class SearchField extends HookConsumerWidget {
const SearchField({
super.key,
required this.textEditingController,
required this.onFieldSubmitted,
required this.onSubmitted,
required this.activeBang,
required this.showSuggestions,
this.focusNode,
this.showBangIcon = true,
this.autofocus = false,
});
@override
@@ -32,20 +39,39 @@ class SearchField extends HookConsumerWidget {
() => textEditingController.text.isNotEmpty,
);
return TextFormField(
final suggestion = useState<String?>(null);
if (showSuggestions) {
useListenableCallback(
textEditingController,
() async {
if (textEditingController.text.isNotEmpty) {
final result = await ref
.read(engineSuggestionsProvider.notifier)
.getAutocompleteSuggestion(textEditingController.text);
suggestion.value = result;
}
},
);
}
return AutoSuggestTextField(
controller: textEditingController,
suggestion: suggestion.value,
enableIMEPersonalizedLearning: !incognitoEnabled,
focusNode: focusNode,
autofocus: autofocus,
decoration: InputDecoration(
border: InputBorder.none,
prefixIcon: (showBangIcon && activeBang != null)
? Padding(
padding: const EdgeInsetsDirectional.all(12.0),
child: BangIcon(activeBang!, iconSize: 24.0),
child: UrlIcon(activeBang!.getUrl(''), iconSize: 24.0),
)
: null,
label: const Text('Search'),
hintText: 'Ask anything...',
// hintText: 'Ask anything...',
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: hasText
? IconButton(
@@ -65,7 +91,7 @@ class SearchField extends HookConsumerWidget {
focusNode!.unfocus();
}
: null,
onFieldSubmitted: onFieldSubmitted,
onSubmitted: onSubmitted,
);
}
}
@@ -0,0 +1,133 @@
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/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/search/domain/providers/engine_suggestions.dart';
import 'package:lensai/features/geckoview/utils/image_helper.dart';
import 'package:lensai/presentation/hooks/cached_future.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:sliver_tools/sliver_tools.dart';
class HistorySuggestions extends HookConsumerWidget {
final TextEditingController searchTextController;
const HistorySuggestions({super.key, required this.searchTextController});
@override
Widget build(BuildContext context, WidgetRef ref) {
final historySuggestions = ref.watch(engineHistorySuggestionsProvider);
useListenableCallback(
searchTextController,
() async {
await ref
.watch(engineSuggestionsProvider.notifier)
.addQuery(searchTextController.text);
},
);
if (historySuggestions.hasValue &&
(historySuggestions.valueOrNull?.isEmpty ?? true)) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return MultiSliver(
children: [
const SliverToBoxAdapter(
child: Divider(),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
'History',
style: Theme.of(context).textTheme.labelSmall,
),
),
),
SliverSkeletonizer(
enabled: historySuggestions.isLoading,
child: historySuggestions.when(
data: (historySuggestions) {
return SliverList.builder(
itemCount: historySuggestions.length,
itemBuilder: (context, index) {
final suggestion = historySuggestions[index];
return HookBuilder(
key: ValueKey(suggestion.id),
builder: (context) {
final icon = useCachedFuture(
() async => suggestion.icon.mapNotNull(tryDecodeImage),
//URL as key
[suggestion.description],
);
return ListTile(
leading: RepaintBoundary(
child: RawImage(
image: icon.data,
height: 24,
width: 24,
),
),
title:
suggestion.title.mapNotNull((title) => Text(title)),
subtitle: suggestion.description.mapNotNull(
(description) => Text(
description,
softWrap: false,
overflow: TextOverflow.fade,
),
),
onTap: () async {
if (suggestion.description != null) {
if (Uri.tryParse(suggestion.description!)
case final Uri url) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: url);
if (context.mounted) {
ref
.read(
bottomSheetControllerProvider.notifier,
)
.dismiss();
context.pop();
}
}
}
},
);
},
);
},
);
},
error: (error, stackTrace) {
return FailureWidget(
title: 'Could not load history',
exception: error,
);
},
loading: () => SliverList.builder(
itemCount: historySuggestions.valueOrNull?.length ?? 3,
itemBuilder: (context, index) {
return const ListTile(
title: Bone.text(),
);
},
),
),
),
],
);
}
}
@@ -0,0 +1,181 @@
import 'package:collection/collection.dart';
import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
import 'package:lensai/features/geckoview/features/search/domain/providers/search_suggestions.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/bang_chips.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:sliver_tools/sliver_tools.dart';
class SearchTermSuggestions extends HookConsumerWidget {
final TextEditingController searchTextController;
final Future<void> Function(String query) submitSearch;
final BangData? activeBang;
final bool showHistory;
final bool showChips;
const SearchTermSuggestions({
required this.searchTextController,
required this.submitSearch,
required this.activeBang,
this.showHistory = true,
this.showChips = true,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchTextIsNotEmpty = useListenableSelector(
searchTextController,
() => searchTextController.text.isNotEmpty,
);
final searchSuggestions = ref.watch(searchSuggestionsProvider());
final searchHistory = ref.watch(searchHistoryProvider);
useListenableCallback(
searchTextController,
() async {
ref
.read(searchSuggestionsProvider().notifier)
.addQuery(searchTextController.text);
},
);
final Widget listSliver;
if (showHistory &&
!searchTextIsNotEmpty &&
(searchHistory.value?.isNotEmpty ?? false)) {
final entries = searchHistory.value!;
listSliver = SliverList.builder(
itemCount: entries.length,
itemBuilder: (context, index) {
final query = entries[index].searchQuery;
return ListTile(
leading: const Icon(Icons.history),
title: Text(query),
onLongPress: () {
searchTextController.text = query;
},
onTap: () async {
await submitSearch(query);
},
trailing: IconButton(
onPressed: () async {
await ref
.read(bangDataRepositoryProvider.notifier)
.removeSearchEntry(query);
},
icon: const Icon(Icons.close),
),
);
},
);
} else {
final prioritizedSuggestions = [
if (searchTextIsNotEmpty) searchTextController.text,
if (searchSuggestions.valueOrNull != null)
...searchSuggestions.valueOrNull!.whereNot(
(suggestion) => suggestion == searchTextController.text,
),
];
listSliver = SliverList.builder(
itemCount: prioritizedSuggestions.length,
itemBuilder: (context, index) {
final suggestion = prioritizedSuggestions[index];
return ListTile(
leading: const Icon(Icons.search),
title: Text(suggestion),
onLongPress: () {
searchTextController.text = suggestion;
},
onTap: () async {
await submitSearch(suggestion);
},
);
},
);
}
return MultiSliver(
children: [
if (showChips)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Search Provider',
style: Theme.of(context).textTheme.labelSmall,
),
BangChips(
activeBang: activeBang,
onSelected: (bang) {
searchTextController.clear();
ref
.read(selectedBangTriggerProvider().notifier)
.setTrigger(bang.trigger);
},
onDeleted: (bang) async {
if (ref.read(selectedBangTriggerProvider()) ==
bang.trigger) {
ref
.read(
selectedBangTriggerProvider().notifier,
)
.clearTrigger();
} else {
final dialogResult = await BangChips.resetBangDialog(
context,
bang.trigger,
);
if (dialogResult == true) {
await ref
.read(bangDataRepositoryProvider.notifier)
.resetFrequency(bang.trigger);
}
}
},
searchTextController: searchTextController,
),
],
),
),
),
SliverToBoxAdapter(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150),
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return CustomScrollView(
shrinkWrap: true,
controller: controller,
slivers: [
listSliver,
],
);
},
),
),
),
],
);
}
}
@@ -0,0 +1,152 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.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/tabs/data/entities/container_filter.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.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:lensai/presentation/widgets/bang_icon.dart';
import 'package:sliver_tools/sliver_tools.dart';
class TabSearch extends HookConsumerWidget {
static const _matchPrefix = '***';
static const _matchSuffix = '***';
final TextEditingController searchTextController;
const TabSearch({required this.searchTextController});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedContainer = useState<ContainerData?>(
ref.read(
selectedContainerDataProvider.select((value) => value.valueOrNull),
),
);
final tabs = ref
.watch(
seamlessFilteredTabPreviewsProvider(
TabSearchPartition.search,
(selectedContainer.value != null)
? ContainerFilterById(containerId: selectedContainer.value!.id)
: ContainerFilterDisabled(),
),
)
.collection;
useListenableCallback(
searchTextController,
() async {
await ref
.read(
tabSearchRepositoryProvider(TabSearchPartition.search).notifier,
)
.addQuery(
searchTextController.text,
// ignore: avoid_redundant_argument_values
matchPrefix: _matchPrefix,
// ignore: avoid_redundant_argument_values
matchSuffix: _matchSuffix,
);
},
);
return MultiSliver(
children: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tabs',
style: Theme.of(context).textTheme.labelSmall,
),
ContainerChips(
displayMenu: false,
selectedContainer: selectedContainer.value,
onSelected: (container) {
selectedContainer.value = container;
},
onDeleted: (container) {
selectedContainer.value = null;
},
containerFilter: (container) => (container.tabCount ?? 0) > 0,
searchTextController: searchTextController,
),
],
),
),
),
SliverList.builder(
itemCount: tabs.length,
itemBuilder: (context, index) {
final result = tabs[index];
final urlHasMatch =
result.highlightedUrl?.contains(_matchPrefix) ?? false;
final bodyHasMatch =
result.content?.contains(_matchPrefix) ?? false;
return ListTile(
leading: RepaintBoundary(
child: (result.icon != null)
? RawImage(
image: result.icon?.value,
height: 24,
width: 24,
)
: UrlIcon(result.url, iconSize: 24),
),
title: result.title.mapNotNull(
(title) => MarkdownBody(
data: title,
styleSheet: MarkdownStyleSheet(
p: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
subtitle: MarkdownBody(
data: (bodyHasMatch && !urlHasMatch)
? result.content!
: result.highlightedUrl ?? result.url.toString(),
styleSheet: MarkdownStyleSheet(
p: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
a: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
decoration: TextDecoration.none,
),
),
),
onTap: () async {
await ref
.read(tabRepositoryProvider.notifier)
.selectTab(result.id);
if (context.mounted) {
ref.read(bottomSheetControllerProvider.notifier).dismiss();
context.pop();
}
},
);
},
),
],
);
}
}
@@ -127,7 +127,8 @@ queryTabsBasic WITH TabQueryResult:
t.id,
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank
bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank,
t.url AS clean_url
FROM tab_fts fts
INNER JOIN
tab t ON t.rowid = fts.rowid
@@ -157,7 +158,8 @@ queryTabsFullContent WITH TabQueryResult:
(
bm25(tab_fts, weights.title_weight, weights.url_weight,
weights.extracted_weight, weights.full_weight)
) AS weighted_rank
) AS weighted_rank,
t.url AS clean_url
FROM tab_fts(:query) fts
INNER JOIN
tab t ON t.rowid = fts.rowid
@@ -1639,7 +1639,7 @@ abstract class _$TabDatabase extends GeneratedDatabase {
required String afterMatch,
required String query}) {
return customSelect(
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?3 OR fts.url LIKE ?3 ORDER BY weighted_rank ASC, t.timestamp DESC',
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank, t.url AS clean_url FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?3 OR fts.url LIKE ?3 ORDER BY weighted_rank ASC, t.timestamp DESC',
variables: [
Variable<String>(beforeMatch),
Variable<String>(afterMatch),
@@ -1652,6 +1652,7 @@ abstract class _$TabDatabase extends GeneratedDatabase {
id: row.read<String>('id'),
title: row.readNullable<String>('title'),
url: row.readNullable<String>('url'),
cleanUrl: row.readNullable<String>('clean_url'),
weightedRank: row.read<double>('weighted_rank'),
));
}
@@ -1663,7 +1664,7 @@ abstract class _$TabDatabase extends GeneratedDatabase {
required int snippetLength,
required String query}) {
return customSelect(
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC',
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank, t.url AS clean_url FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC',
variables: [
Variable<String>(beforeMatch),
Variable<String>(afterMatch),
@@ -1678,6 +1679,7 @@ abstract class _$TabDatabase extends GeneratedDatabase {
id: row.read<String>('id'),
title: row.readNullable<String>('title'),
url: row.readNullable<String>('url'),
cleanUrl: row.readNullable<String>('clean_url'),
extractedContent: row.readNullable<String>('extracted_content'),
fullContent: row.readNullable<String>('full_content'),
weightedRank: row.read<double>('weighted_rank'),
@@ -0,0 +1,23 @@
import 'package:fast_equatable/fast_equatable.dart';
sealed class ContainerFilter with FastEquatable {}
class ContainerFilterById extends ContainerFilter {
final String? containerId;
ContainerFilterById({required this.containerId});
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [containerId];
}
class ContainerFilterDisabled extends ContainerFilter {
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [null];
}
@@ -2,8 +2,9 @@ class TabQueryResult {
final String id;
final String? title;
final String? url;
final String? cleanUrl;
final String? url;
final String? extractedContent;
final String? fullContent;
@@ -13,6 +14,7 @@ class TabQueryResult {
required this.id,
required this.title,
required this.url,
required this.cleanUrl,
this.extractedContent,
this.fullContent,
required this.weightedRank,
@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:lensai/features/geckoview/features/tabs/data/entities/container_filter.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
@@ -37,7 +38,7 @@ Stream<List<ContainerDataWithCount>> containersWithCount(
}
@Riverpod()
AsyncValue<List<ContainerDataWithCount>> filteredContainersWithCount(
AsyncValue<List<ContainerDataWithCount>> matchSortedContainersWithCount(
Ref ref,
String? searchText,
) {
@@ -48,7 +49,7 @@ AsyncValue<List<ContainerDataWithCount>> filteredContainersWithCount(
}
return value.whenData(
(cb) => TokenizedFilter(
(cb) => TokenizedFilter.sort(
items: cb,
toString: (item) => item.name,
query: searchText!,
@@ -61,8 +62,14 @@ AsyncValue<List<ContainerDataWithCount>> filteredContainersWithCount(
@Riverpod()
Stream<List<String>> containerTabIds(
Ref ref,
String? containerId,
ContainerFilter containerFilter,
) {
final db = ref.watch(tabDatabaseProvider);
return db.tabDao.containerTabIds(containerId).watch();
switch (containerFilter) {
case ContainerFilterById(:final containerId):
return db.tabDao.containerTabIds(containerId).watch();
case ContainerFilterDisabled():
return db.tabDao.allTabIds().watch();
}
}
@@ -45,8 +45,8 @@ final containersWithCountProvider =
// ignore: unused_element
typedef ContainersWithCountRef
= AutoDisposeStreamProviderRef<List<ContainerDataWithCount>>;
String _$filteredContainersWithCountHash() =>
r'3a7284df29182a32f9b7831867a3a0b3e171b382';
String _$matchSortedContainersWithCountHash() =>
r'0ed555f0cb72f4990a2841114d72f57d8178d5c3';
/// Copied from Dart SDK
class _SystemHash {
@@ -69,28 +69,29 @@ class _SystemHash {
}
}
/// See also [filteredContainersWithCount].
@ProviderFor(filteredContainersWithCount)
const filteredContainersWithCountProvider = FilteredContainersWithCountFamily();
/// See also [matchSortedContainersWithCount].
@ProviderFor(matchSortedContainersWithCount)
const matchSortedContainersWithCountProvider =
MatchSortedContainersWithCountFamily();
/// See also [filteredContainersWithCount].
class FilteredContainersWithCountFamily
/// See also [matchSortedContainersWithCount].
class MatchSortedContainersWithCountFamily
extends Family<AsyncValue<List<ContainerDataWithCount>>> {
/// See also [filteredContainersWithCount].
const FilteredContainersWithCountFamily();
/// See also [matchSortedContainersWithCount].
const MatchSortedContainersWithCountFamily();
/// See also [filteredContainersWithCount].
FilteredContainersWithCountProvider call(
/// See also [matchSortedContainersWithCount].
MatchSortedContainersWithCountProvider call(
String? searchText,
) {
return FilteredContainersWithCountProvider(
return MatchSortedContainersWithCountProvider(
searchText,
);
}
@override
FilteredContainersWithCountProvider getProviderOverride(
covariant FilteredContainersWithCountProvider provider,
MatchSortedContainersWithCountProvider getProviderOverride(
covariant MatchSortedContainersWithCountProvider provider,
) {
return call(
provider.searchText,
@@ -109,33 +110,33 @@ class FilteredContainersWithCountFamily
_allTransitiveDependencies;
@override
String? get name => r'filteredContainersWithCountProvider';
String? get name => r'matchSortedContainersWithCountProvider';
}
/// See also [filteredContainersWithCount].
class FilteredContainersWithCountProvider
/// See also [matchSortedContainersWithCount].
class MatchSortedContainersWithCountProvider
extends AutoDisposeProvider<AsyncValue<List<ContainerDataWithCount>>> {
/// See also [filteredContainersWithCount].
FilteredContainersWithCountProvider(
/// See also [matchSortedContainersWithCount].
MatchSortedContainersWithCountProvider(
String? searchText,
) : this._internal(
(ref) => filteredContainersWithCount(
ref as FilteredContainersWithCountRef,
(ref) => matchSortedContainersWithCount(
ref as MatchSortedContainersWithCountRef,
searchText,
),
from: filteredContainersWithCountProvider,
name: r'filteredContainersWithCountProvider',
from: matchSortedContainersWithCountProvider,
name: r'matchSortedContainersWithCountProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$filteredContainersWithCountHash,
dependencies: FilteredContainersWithCountFamily._dependencies,
: _$matchSortedContainersWithCountHash,
dependencies: MatchSortedContainersWithCountFamily._dependencies,
allTransitiveDependencies:
FilteredContainersWithCountFamily._allTransitiveDependencies,
MatchSortedContainersWithCountFamily._allTransitiveDependencies,
searchText: searchText,
);
FilteredContainersWithCountProvider._internal(
MatchSortedContainersWithCountProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
@@ -150,13 +151,13 @@ class FilteredContainersWithCountProvider
@override
Override overrideWith(
AsyncValue<List<ContainerDataWithCount>> Function(
FilteredContainersWithCountRef provider)
MatchSortedContainersWithCountRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: FilteredContainersWithCountProvider._internal(
(ref) => create(ref as FilteredContainersWithCountRef),
override: MatchSortedContainersWithCountProvider._internal(
(ref) => create(ref as MatchSortedContainersWithCountRef),
from: from,
name: null,
dependencies: null,
@@ -170,12 +171,12 @@ class FilteredContainersWithCountProvider
@override
AutoDisposeProviderElement<AsyncValue<List<ContainerDataWithCount>>>
createElement() {
return _FilteredContainersWithCountProviderElement(this);
return _MatchSortedContainersWithCountProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is FilteredContainersWithCountProvider &&
return other is MatchSortedContainersWithCountProvider &&
other.searchText == searchText;
}
@@ -190,23 +191,23 @@ class FilteredContainersWithCountProvider
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin FilteredContainersWithCountRef
mixin MatchSortedContainersWithCountRef
on AutoDisposeProviderRef<AsyncValue<List<ContainerDataWithCount>>> {
/// The parameter `searchText` of this provider.
String? get searchText;
}
class _FilteredContainersWithCountProviderElement
class _MatchSortedContainersWithCountProviderElement
extends AutoDisposeProviderElement<AsyncValue<List<ContainerDataWithCount>>>
with FilteredContainersWithCountRef {
_FilteredContainersWithCountProviderElement(super.provider);
with MatchSortedContainersWithCountRef {
_MatchSortedContainersWithCountProviderElement(super.provider);
@override
String? get searchText =>
(origin as FilteredContainersWithCountProvider).searchText;
(origin as MatchSortedContainersWithCountProvider).searchText;
}
String _$containerTabIdsHash() => r'7545a6c500b1832bf81c0838e257dfe5e051463d';
String _$containerTabIdsHash() => r'6472a865c287d7f87ec49a401c8184bfe7528ab8';
/// See also [containerTabIds].
@ProviderFor(containerTabIds)
@@ -219,10 +220,10 @@ class ContainerTabIdsFamily extends Family<AsyncValue<List<String>>> {
/// See also [containerTabIds].
ContainerTabIdsProvider call(
String? containerId,
ContainerFilter containerFilter,
) {
return ContainerTabIdsProvider(
containerId,
containerFilter,
);
}
@@ -231,7 +232,7 @@ class ContainerTabIdsFamily extends Family<AsyncValue<List<String>>> {
covariant ContainerTabIdsProvider provider,
) {
return call(
provider.containerId,
provider.containerFilter,
);
}
@@ -254,11 +255,11 @@ class ContainerTabIdsFamily extends Family<AsyncValue<List<String>>> {
class ContainerTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
/// See also [containerTabIds].
ContainerTabIdsProvider(
String? containerId,
ContainerFilter containerFilter,
) : this._internal(
(ref) => containerTabIds(
ref as ContainerTabIdsRef,
containerId,
containerFilter,
),
from: containerTabIdsProvider,
name: r'containerTabIdsProvider',
@@ -269,7 +270,7 @@ class ContainerTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
dependencies: ContainerTabIdsFamily._dependencies,
allTransitiveDependencies:
ContainerTabIdsFamily._allTransitiveDependencies,
containerId: containerId,
containerFilter: containerFilter,
);
ContainerTabIdsProvider._internal(
@@ -279,10 +280,10 @@ class ContainerTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.containerId,
required this.containerFilter,
}) : super.internal();
final String? containerId;
final ContainerFilter containerFilter;
@override
Override overrideWith(
@@ -297,7 +298,7 @@ class ContainerTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
containerId: containerId,
containerFilter: containerFilter,
),
);
}
@@ -309,13 +310,14 @@ class ContainerTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
@override
bool operator ==(Object other) {
return other is ContainerTabIdsProvider && other.containerId == containerId;
return other is ContainerTabIdsProvider &&
other.containerFilter == containerFilter;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, containerId.hashCode);
hash = _SystemHash.combine(hash, containerFilter.hashCode);
return _SystemHash.finish(hash);
}
@@ -324,8 +326,8 @@ class ContainerTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ContainerTabIdsRef on AutoDisposeStreamProviderRef<List<String>> {
/// The parameter `containerId` of this provider.
String? get containerId;
/// The parameter `containerFilter` of this provider.
ContainerFilter get containerFilter;
}
class _ContainerTabIdsProviderElement
@@ -334,7 +336,8 @@ class _ContainerTabIdsProviderElement
_ContainerTabIdsProviderElement(super.provider);
@override
String? get containerId => (origin as ContainerTabIdsProvider).containerId;
ContainerFilter get containerFilter =>
(origin as ContainerTabIdsProvider).containerFilter;
}
// 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
@@ -6,6 +6,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tab_search.g.dart';
enum TabSearchPartition { preview, search }
@Riverpod()
class TabSearchRepository extends _$TabSearchRepository {
Future<void> addQuery(
@@ -35,7 +37,7 @@ class TabSearchRepository extends _$TabSearchRepository {
}
@override
Future<List<TabQueryResult>?> build() {
Future<List<TabQueryResult>?> build(TabSearchPartition partition) {
return Future.value();
}
}
@@ -7,21 +7,174 @@ part of 'tab_search.dart';
// **************************************************************************
String _$tabSearchRepositoryHash() =>
r'57d130f50aa3feb7a31d27f9a5b54234e0e7064f';
r'd442c384a5a867f4ff28465f524a6ef67521d5ae';
/// 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));
}
}
abstract class _$TabSearchRepository
extends BuildlessAutoDisposeAsyncNotifier<List<TabQueryResult>?> {
late final TabSearchPartition partition;
FutureOr<List<TabQueryResult>?> build(
TabSearchPartition partition,
);
}
/// See also [TabSearchRepository].
@ProviderFor(TabSearchRepository)
final tabSearchRepositoryProvider = AutoDisposeAsyncNotifierProvider<
TabSearchRepository, List<TabQueryResult>?>.internal(
TabSearchRepository.new,
name: r'tabSearchRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$tabSearchRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
const tabSearchRepositoryProvider = TabSearchRepositoryFamily();
typedef _$TabSearchRepository = AutoDisposeAsyncNotifier<List<TabQueryResult>?>;
/// See also [TabSearchRepository].
class TabSearchRepositoryFamily
extends Family<AsyncValue<List<TabQueryResult>?>> {
/// See also [TabSearchRepository].
const TabSearchRepositoryFamily();
/// See also [TabSearchRepository].
TabSearchRepositoryProvider call(
TabSearchPartition partition,
) {
return TabSearchRepositoryProvider(
partition,
);
}
@override
TabSearchRepositoryProvider getProviderOverride(
covariant TabSearchRepositoryProvider provider,
) {
return call(
provider.partition,
);
}
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'tabSearchRepositoryProvider';
}
/// See also [TabSearchRepository].
class TabSearchRepositoryProvider extends AutoDisposeAsyncNotifierProviderImpl<
TabSearchRepository, List<TabQueryResult>?> {
/// See also [TabSearchRepository].
TabSearchRepositoryProvider(
TabSearchPartition partition,
) : this._internal(
() => TabSearchRepository()..partition = partition,
from: tabSearchRepositoryProvider,
name: r'tabSearchRepositoryProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$tabSearchRepositoryHash,
dependencies: TabSearchRepositoryFamily._dependencies,
allTransitiveDependencies:
TabSearchRepositoryFamily._allTransitiveDependencies,
partition: partition,
);
TabSearchRepositoryProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.partition,
}) : super.internal();
final TabSearchPartition partition;
@override
FutureOr<List<TabQueryResult>?> runNotifierBuild(
covariant TabSearchRepository notifier,
) {
return notifier.build(
partition,
);
}
@override
Override overrideWith(TabSearchRepository Function() create) {
return ProviderOverride(
origin: this,
override: TabSearchRepositoryProvider._internal(
() => create()..partition = partition,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
partition: partition,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<TabSearchRepository,
List<TabQueryResult>?> createElement() {
return _TabSearchRepositoryProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TabSearchRepositoryProvider && other.partition == partition;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, partition.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin TabSearchRepositoryRef
on AutoDisposeAsyncNotifierProviderRef<List<TabQueryResult>?> {
/// The parameter `partition` of this provider.
TabSearchPartition get partition;
}
class _TabSearchRepositoryProviderElement
extends AutoDisposeAsyncNotifierProviderElement<TabSearchRepository,
List<TabQueryResult>?> with TabSearchRepositoryRef {
_TabSearchRepositoryProviderElement(super.provider);
@override
TabSearchPartition get partition =>
(origin as TabSearchRepositoryProvider).partition;
}
// 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
@@ -18,6 +18,7 @@ class ContainerChips extends HookConsumerWidget {
final bool displayMenu;
final ContainerData? selectedContainer;
final bool Function(ContainerDataWithCount)? containerFilter;
final void Function(ContainerDataWithCount)? onSelected;
final void Function(ContainerDataWithCount)? onDeleted;
@@ -27,6 +28,7 @@ class ContainerChips extends HookConsumerWidget {
required this.selectedContainer,
required this.onSelected,
required this.onDeleted,
this.containerFilter,
this.searchTextController,
this.displayMenu = true,
});
@@ -39,12 +41,16 @@ class ContainerChips extends HookConsumerWidget {
);
final containersAsync =
ref.watch(filteredContainersWithCountProvider(searchText));
ref.watch(matchSortedContainersWithCountProvider(searchText));
final dragTargetTabId = useValueNotifier<String?>(null);
return containersAsync.when(
data: (availableContainers) {
data: (containers) {
final availableContainers = containerFilter
.mapNotNull((filter) => containers.where(filter).toList()) ??
containers;
if (selectedContainer == null &&
availableContainers.isEmpty &&
!displayMenu) {
@@ -1,12 +1,24 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:lensai/utils/lru_cache.dart';
import 'package:xxh3/xxh3.dart';
final _cache = LRUCache<int, Image>(100);
Future<Image?> tryDecodeImage(
Uint8List bytes, {
int? targetWidth,
int? targetHeight,
bool allowUpscaling = true,
}) async {
final digest = xxh3(bytes);
final cached = _cache.get(digest);
if (cached != null) {
return cached;
}
try {
final codec = await instantiateImageCodec(
bytes,
@@ -19,6 +31,7 @@ Future<Image?> tryDecodeImage(
final image = frameInfo.image;
if (image.width > 0) {
_cache.set(digest, image);
return image;
}
} catch (e) {
@@ -16,7 +16,9 @@ class SearchSuggestionsRepository extends _$SearchSuggestionsRepository {
SearchSuggestionsRepository() : _cache = LRUCache(100);
void addQuery(String query) {
_queryStreamController.add(query);
if (!_queryStreamController.isClosed) {
_queryStreamController.add(query);
}
}
@override
@@ -7,7 +7,7 @@ part of 'search_suggestions.dart';
// **************************************************************************
String _$searchSuggestionsRepositoryHash() =>
r'bc6dc862ae7d887b404972abbbee3c1b27a64d2e';
r'be75d075c3479c3d5e462211dec8bbd21b106820';
/// Copied from Dart SDK
class _SystemHash {
@@ -1,292 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/domain/providers/search.dart';
import 'package:lensai/features/bangs/domain/repositories/data.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/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.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/features/search/domain/providers/search_suggestions.dart';
import 'package:lensai/features/search/presentation/widgets/bang_chips.dart';
import 'package:lensai/features/search/presentation/widgets/search_field.dart';
import 'package:lensai/features/search/presentation/widgets/search_suggestion_list.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart';
import 'package:skeletonizer/skeletonizer.dart';
class SearchScreen extends HookConsumerWidget {
static const _matchPrefix = '***';
final String? initialSearchText;
const SearchScreen({required this.initialSearchText});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final searchTextController =
useTextEditingController(text: initialSearchText);
useListenableCallback(
searchTextController,
() async {
ref
.read(searchSuggestionsProvider().notifier)
.addQuery(searchTextController.text);
await ref.read(tabSearchRepositoryProvider.notifier).addQuery(
searchTextController.text,
// ignore: avoid_redundant_argument_values
matchPrefix: _matchPrefix,
// ignore: avoid_redundant_argument_values
matchSuffix: _matchPrefix,
);
},
);
final globalSelectedContainer = ref.watch(
selectedContainerDataProvider.select((value) => value.valueOrNull),
);
final defaultSearchBang = ref.watch(
defaultSearchBangDataProvider.select((value) => value.valueOrNull),
);
final selectedBang = useState<BangData?>(null);
final activeBang = selectedBang.value ?? defaultSearchBang;
final showBangIcon = useState(false);
ref.listen(
selectedBangDataProvider(),
(previous, next) {
if (next.hasValue) {
if ((previous?.hasValue ?? false) && previous!.value != next.value) {
showBangIcon.value = true;
}
selectedBang.value = next.value;
}
},
);
Future<void> submitSearch(String query) async {
if (activeBang != null && (formKey.currentState?.validate() == true)) {
final searchUri = await ref.read(
triggerBangSearchProvider(activeBang, query).future,
);
await ref.read(tabRepositoryProvider.notifier).addTab(url: searchUri);
if (context.mounted) {
context.pop();
}
}
}
final selectedContainer = useState<ContainerData?>(globalSelectedContainer);
final availableTabs = ref
.watch(
availableTabIdsProvider(selectedContainer.value?.id).select(
(value) => EquatableCollection(value, immutable: true),
),
)
.collection;
return Scaffold(
body: Form(
key: formKey,
child: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
pinned: true,
automaticallyImplyLeading: false,
title: SearchField(
showBangIcon: showBangIcon.value,
textEditingController: searchTextController,
onFieldSubmitted: (text) {},
activeBang: activeBang,
),
),
const SliverToBoxAdapter(
child: Divider(),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Search Provider',
style: Theme.of(context).textTheme.labelSmall,
),
BangChips(
activeBang: activeBang,
onSelected: (bang) {
ref
.read(selectedBangTriggerProvider().notifier)
.setTrigger(bang.trigger);
},
onDeleted: (bang) async {
if (ref.read(selectedBangTriggerProvider()) ==
bang.trigger) {
ref
.read(
selectedBangTriggerProvider().notifier,
)
.clearTrigger();
} else {
final dialogResult = await BangChips.resetBangDialog(
context,
bang.trigger,
);
if (dialogResult == true) {
await ref
.read(bangDataRepositoryProvider.notifier)
.resetFrequency(bang.trigger);
}
}
},
searchTextController: searchTextController,
),
],
),
),
),
SearchSuggestionList(
searchTextController: searchTextController,
submitSearch: submitSearch,
),
const SliverToBoxAdapter(
child: Divider(),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tabs',
style: Theme.of(context).textTheme.labelSmall,
),
ContainerChips(
displayMenu: false,
selectedContainer: selectedContainer.value,
onSelected: (container) {
selectedContainer.value = container;
},
onDeleted: (container) {
selectedContainer.value = null;
},
searchTextController: searchTextController,
),
],
),
),
),
Consumer(
builder: (context, ref, child) {
final tabs = ref.watch(tabSearchRepositoryProvider);
return SliverSkeletonizer(
enabled: tabs.isLoading,
child: tabs.when(
data: (data) {
if (data == null) {
return const SliverToBoxAdapter(
child: SizedBox.shrink(),
);
}
return SliverList.builder(
itemCount: data.length,
itemBuilder: (context, index) {
final result = data[index];
final headHasMatch =
result.title?.contains(_matchPrefix) ?? false;
final bodyResult =
result.extractedContent ?? result.fullContent;
return ListTile(
title: result.title.mapNotNull(
(title) => MarkdownBody(data: title),
),
subtitle: (!headHasMatch && bodyResult != null)
? MarkdownBody(data: bodyResult)
: null,
);
},
);
},
error: (error, stackTrace) {
return FailureWidget(
title: 'Could not load tabs',
exception: error,
);
},
loading: () => SliverList.builder(
itemCount: tabs.valueOrNull?.length ?? 3,
itemBuilder: (context, index) {
return ListTile(
title: Bone.text(),
);
},
),
),
);
},
),
SliverList.builder(
itemCount: availableTabs.length,
itemBuilder: (context, index) {
return Consumer(
key: ValueKey(availableTabs[index]),
builder: (context, ref, child) {
final tab =
ref.watch(tabStateProvider(availableTabs[index]));
if (tab == null) {
return const SizedBox.shrink();
}
return ListTile(
leading: RepaintBoundary(
child: RawImage(
image: tab.icon?.value,
height: 24,
width: 24,
),
),
title: Text(tab.title),
subtitle: Text(
tab.url.toString(),
softWrap: false,
overflow: TextOverflow.fade,
),
);
},
);
},
),
],
),
),
);
}
}
@@ -1,90 +0,0 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart';
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
class SearchSuggestionList extends HookConsumerWidget {
final TextEditingController searchTextController;
final Future<void> Function(String query) submitSearch;
final bool showHistory;
const SearchSuggestionList({
required this.searchTextController,
required this.submitSearch,
this.showHistory = true,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
useListenableSelector(
searchTextController,
() => searchTextController.text.isNotEmpty,
);
final suggestions = useStream(ref.watch(searchSuggestionsProvider()));
final searchHistory = ref.watch(searchHistoryProvider);
final searchText = searchTextController.text;
if (showHistory &&
(!searchText.isNotEmpty || !suggestions.hasData) &&
(searchHistory.value?.isNotEmpty ?? false)) {
final entries = searchHistory.value!;
return SliverList.builder(
itemCount: entries.length,
itemBuilder: (context, index) {
final query = entries[index].searchQuery;
return ListTile(
leading: const Icon(Icons.history),
title: Text(query),
onLongPress: () {
searchTextController.text = query;
},
onTap: () async {
await submitSearch(query);
},
trailing: IconButton(
onPressed: () async {
await ref
.read(bangDataRepositoryProvider.notifier)
.removeSearchEntry(query);
},
icon: const Icon(Icons.close),
),
);
},
);
}
final prioritizedSuggestions = [
if (searchText.isNotEmpty) searchText,
if (suggestions.data != null)
...suggestions.data!.whereNot((suggestion) => suggestion == searchText),
];
return SliverList.builder(
itemCount: prioritizedSuggestions.length,
itemBuilder: (context, index) {
final suggestion = prioritizedSuggestions[index];
return ListTile(
leading: const Icon(Icons.search),
title: Text(suggestion),
onLongPress: () {
searchTextController.text = suggestion;
},
onTap: () async {
await submitSearch(suggestion);
},
);
},
);
}
}
@@ -24,7 +24,83 @@ class TokenizedFilter<T> {
);
}
factory TokenizedFilter({
static double _calculateScore(
List<String> queryTokens,
List<String> targetTokens,
) {
if (queryTokens.isEmpty || targetTokens.isEmpty) return 0.0;
var score = 0.0;
var exactMatches = 0;
var partialMatches = 0;
for (final queryToken in queryTokens) {
var hasMatch = false;
for (int i = 0; i < targetTokens.length; i++) {
final targetToken = targetTokens[i];
if (targetToken == queryToken) {
// Exact match gets 1.0 points
score += 1.0;
exactMatches++;
hasMatch = true;
break;
} else if (targetToken.contains(queryToken)) {
// Partial match gets points based on match length ratio
final ratio = queryToken.length / targetToken.length;
score += ratio * 0.8; // Partial matches worth 80% of exact matches
partialMatches++;
hasMatch = true;
break;
}
}
if (!hasMatch) {
score -= 0.2; // Penalty for unmatched query tokens
}
}
// Normalize score based on number of query tokens and matches
final matchRatio =
(exactMatches + partialMatches * 0.8) / queryTokens.length;
score = score / queryTokens.length; // Normalize by query length
// Apply match ratio as a multiplier
score *= matchRatio;
// Ensure score is between 0 and 1
return score.clamp(0.0, 1.0);
}
factory TokenizedFilter.sort({
required Iterable<T> items,
required ToString<T> toString,
required String query,
}) {
final queryTokens = _tokenize(query);
final scoredItems = items.map((item) {
final itemString = toString(item);
if (itemString == null || itemString.isEmpty) {
return (item, 0.0);
}
final itemTokens = _tokenize(itemString);
final score = _calculateScore(queryTokens, itemTokens);
return (item, score);
}).toList();
// Sort by score in descending order
scoredItems.sort((a, b) => b.$2.compareTo(a.$2));
// Extract just the items in sorted order
final sorted = scoredItems.map((e) => e.$1).toList();
return TokenizedFilter._(items, sorted);
}
factory TokenizedFilter.remove({
required Iterable<T> items,
required ToString<T> toString,
required String query,
@@ -2,11 +2,13 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
AsyncSnapshot<T> useCachedFuture<T>(
Future<T> Function() valueBuilder,
) {
Future<T> Function() valueBuilder, [
List<Object?> keys = const <Object>[],
]) {
// ignore: discarded_futures
final cachedFuture = useMemoized(
valueBuilder,
keys,
);
return useFuture(cachedFuture);
}
@@ -1,8 +1,11 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void useListenableCallback(Listenable? listenable, void Function() callback,
[List<Object?>? keys]) {
void useListenableCallback(
Listenable? listenable,
void Function() callback, [
List<Object?>? keys,
]) {
useEffect(
() {
listenable?.addListener(callback);
@@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lensai/extensions/nullable.dart';
class AutoSuggestTextField extends HookWidget {
final TextEditingController controller;
final String? suggestion;
final TextStyle? style;
final TextStyle? labelStyle;
final InputDecoration? decoration;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final TextCapitalization textCapitalization;
final bool autofocus;
final bool obscureText;
final int? maxLines;
final int? minLines;
final int? maxLength;
final ValueChanged<String>? onChanged;
final VoidCallback? onEditingComplete;
final ValueChanged<String>? onSubmitted;
final List<TextInputFormatter>? inputFormatters;
final bool? enabled;
final FocusNode? focusNode;
final Color? cursorColor;
final Color? suggestionHighlightColor;
final bool? enableIMEPersonalizedLearning;
final TapRegionCallback? onTapOutside;
const AutoSuggestTextField({
super.key,
required this.controller,
this.suggestion,
this.style,
this.labelStyle,
this.decoration,
this.keyboardType,
this.textInputAction,
this.textCapitalization = TextCapitalization.none,
this.autofocus = false,
this.obscureText = false,
this.maxLines = 1,
this.minLines,
this.maxLength,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
this.inputFormatters,
this.enabled,
this.focusNode,
this.cursorColor,
this.suggestionHighlightColor,
this.enableIMEPersonalizedLearning = true,
this.onTapOutside,
});
bool _suggestionHasMatch() =>
suggestion != null &&
controller.text.isNotEmpty &&
suggestion!.startsWith(controller.text);
@override
Widget build(BuildContext context) {
final baseDecoration = decoration ?? const InputDecoration();
return Stack(
children: [
if (suggestion != null)
AbsorbPointer(
child: TextField(
decoration: baseDecoration.copyWith(
floatingLabelBehavior: FloatingLabelBehavior.never,
label: HookBuilder(
builder: (context) {
final text = useListenableSelector(
controller,
() => controller.text,
);
useEffect(() {
TextSelection? lastSelection;
void handleSelectionChange() {
if (lastSelection != controller.selection) {
lastSelection = controller.selection;
if (lastSelection!.start != lastSelection!.end) {
if (_suggestionHasMatch()) {
controller.value = controller.value.copyWith(
text: suggestion,
selection: lastSelection!.expandTo(
TextPosition(offset: suggestion!.length),
),
);
}
}
}
}
controller.addListener(handleSelectionChange);
return () =>
controller.removeListener(handleSelectionChange);
});
if (!_suggestionHasMatch()) {
return const SizedBox.shrink();
}
return RichText(
text: TextSpan(
text: text,
style: (style ?? Theme.of(context).textTheme.bodyLarge)
?.copyWith(color: Colors.transparent),
children: <TextSpan>[
TextSpan(
text: suggestion!.substring(text.length),
style: TextStyle(
height: 1.2,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
backgroundColor: suggestionHighlightColor ??
Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.40),
),
),
],
),
);
},
),
alignLabelWithHint: true,
),
),
),
TextField(
controller: controller,
focusNode: focusNode,
decoration: baseDecoration.copyWith(
label: baseDecoration.label ?? const Text(''),
floatingLabelBehavior: baseDecoration.floatingLabelBehavior ??
FloatingLabelBehavior.never,
),
style: style,
keyboardType: keyboardType,
textInputAction: textInputAction,
textCapitalization: textCapitalization,
autofocus: autofocus,
obscureText: obscureText,
maxLines: maxLines,
minLines: minLines,
maxLength: maxLength,
onChanged: onChanged,
onEditingComplete: onEditingComplete,
onSubmitted: onSubmitted.mapNotNull(
(onSubmitted) => (value) {
if (_suggestionHasMatch()) {
onSubmitted(suggestion!);
} else {
onSubmitted(value);
}
},
),
inputFormatters: inputFormatters,
enabled: enabled,
cursorColor: cursorColor,
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning ?? true,
onTapOutside: onTapOutside,
),
],
);
}
}
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/domain/services/generic_website.dart';
import 'package:lensai/presentation/hooks/cached_future.dart';
import 'package:skeletonizer/skeletonizer.dart';
class UrlIcon extends HookConsumerWidget {
final double iconSize;
final Uri url;
const UrlIcon(this.url, {required this.iconSize, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final icon = useCachedFuture(
() async =>
ref.read(genericWebsiteServiceProvider.notifier).getUrlIcon(url),
[url],
);
return Skeletonizer(
enabled: !icon.hasData,
child: SizedBox.square(
dimension: iconSize,
child: (icon.data != null)
? RepaintBoundary(
child: RawImage(
image: icon.data?.image.value,
height: iconSize,
width: iconSize,
fit: BoxFit.fill,
),
)
: Icon(
MdiIcons.web,
size: iconSize,
),
),
);
}
}
+1
View File
@@ -63,6 +63,7 @@ dependencies:
rxdart: ^0.28.0
share_plus: ^10.1.4
skeletonizer: ^1.4.3
sliver_tools: ^0.2.12
speech_to_text_google_dialog:
git:
url: https://github.com/FaFre/speech_to_text_google_dialog.git
@@ -1,7 +1,9 @@
package eu.lensai.flutter_mozilla_components.api
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.api.GeckoDeleteBrowsingDataControllerImpl.Companion
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
import eu.lensai.flutter_mozilla_components.pigeons.AutocompleteResult
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestion
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
@@ -10,6 +12,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.concept.engine.Engine
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
class GeckoSuggestionApiImpl(
@@ -23,7 +27,28 @@ class GeckoSuggestionApiImpl(
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
override fun onInputChanged(text: String, providers: List<GeckoSuggestionType>) {
override fun getAutocompleteSuggestion(
query: String,
callback: (Result<AutocompleteResult?>) -> Unit
) {
coroutineScope.launch {
withContext(Dispatchers.Main) {
val suggestion = components.core.historyStorage.getAutocompleteSuggestion(query)
callback(Result.success( suggestion?.let { AutocompleteResult(
input = it.input,
url = it.url,
text = it.text,
totalItems = it.totalItems.toLong(),
source = it.source
)}
))
}
}
}
override fun querySuggestions(text: String, providers: List<GeckoSuggestionType>) {
for(provider in providers) {
coroutineScope.launch {
val results = when(provider) {
@@ -1157,6 +1157,36 @@ data class GeckoEngineSettings (
)
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class AutocompleteResult (
val input: String,
val text: String,
val url: String,
val source: String,
val totalItems: Long
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): AutocompleteResult {
val input = pigeonVar_list[0] as String
val text = pigeonVar_list[1] as String
val url = pigeonVar_list[2] as String
val source = pigeonVar_list[3] as String
val totalItems = pigeonVar_list[4] as Long
return AutocompleteResult(input, text, url, source, totalItems)
}
}
fun toList(): List<Any?> {
return listOf(
input,
text,
url,
source,
totalItems,
)
}
}
private open class GeckoPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
@@ -1365,6 +1395,11 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
GeckoEngineSettings.fromList(it)
}
}
170.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
AutocompleteResult.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
@@ -1534,6 +1569,10 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
stream.write(169)
writeValue(stream, value.toList())
}
is AutocompleteResult -> {
stream.write(170)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
@@ -3129,7 +3168,8 @@ class GeckoAddonEvents(private val binaryMessenger: BinaryMessenger, private val
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoSuggestionApi {
fun onInputChanged(text: String, providers: List<GeckoSuggestionType>)
fun getAutocompleteSuggestion(query: String, callback: (Result<AutocompleteResult?>) -> Unit)
fun querySuggestions(text: String, providers: List<GeckoSuggestionType>)
companion object {
/** The codec used by GeckoSuggestionApi. */
@@ -3141,14 +3181,34 @@ interface GeckoSuggestionApi {
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoSuggestionApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.onInputChanged$separatedMessageChannelSuffix", codec)
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.getAutocompleteSuggestion$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val queryArg = args[0] as String
api.getAutocompleteSuggestion(queryArg) { result: Result<AutocompleteResult?> ->
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<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.querySuggestions$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val textArg = args[0] as String
val providersArg = args[1] as List<GeckoSuggestionType>
val wrapped: List<Any?> = try {
api.onInputChanged(textArg, providersArg)
api.querySuggestions(textArg, providersArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
@@ -3,154 +3,155 @@
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<TextView
android:id="@+id/details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
android:layout_marginBottom="20dp"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
tools:text="@tools:sample/lorem/random" />
android:id="@+id/author_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/details"
android:text="@string/mozac_feature_addons_author" />
<TextView
android:id="@+id/author_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/details"
android:text="@string/mozac_feature_addons_author" />
<TextView
android:id="@+id/author_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/details"
android:layout_alignParentEnd="true"
tools:text="@tools:sample/full_names" />
android:id="@+id/author_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/details"
android:layout_alignParentEnd="true"
tools:text="@tools:sample/full_names" />
<View
android:id="@+id/author_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/author_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
android:id="@+id/author_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/author_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/version_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/author_divider"
android:text="@string/mozac_feature_addons_version" />
android:id="@+id/version_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/author_divider"
android:text="@string/mozac_feature_addons_version" />
<TextView
android:id="@+id/version_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/author_divider"
android:layout_alignParentEnd="true"
tools:text="1.2.3" />
android:id="@+id/version_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/author_divider"
android:layout_alignParentEnd="true"
tools:text="1.2.3" />
<View
android:id="@+id/version_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/version_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
android:id="@+id/version_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/version_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/last_updated_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/version_divider"
android:text="@string/mozac_feature_addons_last_updated" />
android:id="@+id/last_updated_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/version_divider"
android:text="@string/mozac_feature_addons_last_updated" />
<TextView
android:id="@+id/last_updated_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/version_divider"
android:layout_alignParentEnd="true"
tools:text="Oct 16, 2019" />
android:id="@+id/last_updated_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/version_divider"
android:layout_alignParentEnd="true"
tools:text="Oct 16, 2019" />
<View
android:id="@+id/last_updated_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/last_updated_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
android:id="@+id/last_updated_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/last_updated_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/home_page_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/last_updated_divider"
android:text="@string/mozac_feature_addons_home_page" />
android:id="@+id/home_page_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/last_updated_divider"
android:text="@string/mozac_feature_addons_home_page" />
<ImageView
android:id="@+id/home_page_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/last_updated_divider"
android:layout_alignParentEnd="true"
android:contentDescription="@string/mozac_feature_addons_home_page"
android:src="@drawable/mozac_ic_link_24"
app:tint="@android:color/black" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/home_page_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/last_updated_divider"
android:layout_alignParentEnd="true"
android:contentDescription="@string/mozac_feature_addons_home_page"
app:srcCompat="@drawable/mozac_ic_link_24"
app:tint="@android:color/black" />
<View
android:id="@+id/home_page_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/home_page_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
android:id="@+id/home_page_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/home_page_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/rating_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/home_page_divider"
android:text="@string/mozac_feature_addons_rating" />
android:id="@+id/rating_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/home_page_divider"
android:text="@string/mozac_feature_addons_rating" />
<RatingBar
android:id="@+id/rating_view"
style="@style/Widget.AppCompat.RatingBar.Small"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_below="@+id/home_page_divider"
android:layout_toStartOf="@+id/users_count"
android:isIndicator="true"
android:numStars="5" />
android:id="@+id/rating_view"
style="@style/Widget.AppCompat.RatingBar.Small"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_below="@+id/home_page_divider"
android:layout_toStartOf="@+id/users_count"
android:isIndicator="true"
android:numStars="5" />
<TextView
android:id="@+id/users_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/home_page_divider"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
tools:text="591,642" />
android:id="@+id/users_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/home_page_divider"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
tools:text="591,642" />
</RelativeLayout>
</ScrollView>
@@ -7,5 +7,5 @@
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:ignore="MergeRootFrame" />
@@ -4,29 +4,30 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".addons.PermissionsDetailsActivity">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".addons.PermissionsDetailsActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/add_ons_permissions"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:id="@+id/add_ons_permissions"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/learn_more_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/add_ons_permissions"
app:drawableEndCompat="@drawable/mozac_ic_link_24"
android:padding="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground"
android:text="@string/mozac_feature_addons_learn_more"
app:drawableTint="@android:color/black"
android:textColor="@android:color/black" />
android:id="@+id/learn_more_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/add_ons_permissions"
android:background="?attr/selectableItemBackground"
android:padding="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/mozac_feature_addons_learn_more"
android:textColor="@android:color/black"
app:drawableEndCompat="@drawable/mozac_ic_link_24"
app:drawableTint="@android:color/black" />
</RelativeLayout>
@@ -8,4 +8,5 @@
android:id="@+id/addonSettingsContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:ignore="MergeRootFrame" />
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
@@ -77,7 +77,7 @@
android:layout_gravity="center_vertical|end"
android:layout_below="@+id/permissions"
android:background="?android:attr/selectableItemBackground"
android:checked="false"
android:checked="true"
android:clickable="true"
android:focusable="true"
android:text="@string/mozac_feature_addons_settings_allow_in_private_browsing"
@@ -85,12 +85,13 @@
android:textSize="18sp"/>
<Button
android:id="@+id/remove_add_on"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allow_in_private_browsing_switch"
android:layout_marginTop="16dp"
android:textColor="@color/photonRed50"
android:text="@string/mozac_feature_addons_remove" />
android:id="@+id/remove_add_on"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allow_in_private_browsing_switch"
android:layout_marginTop="16dp"
android:text="@string/mozac_feature_addons_remove"
android:textAlignment="center"
android:textColor="@color/photonRed50" />
</RelativeLayout>
</ScrollView>
@@ -4,28 +4,25 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/add_on_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/add_on_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical">
<TextView
android:id="@+id/permission"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:textColor="@android:color/black"
tools:text="Access your data for all websites" />
android:id="@+id/permission"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
tools:text="Access your data for all websites" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
</LinearLayout>
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:importantForAccessibility="no" />
</LinearLayout>
@@ -2,23 +2,43 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/addonProgressOverlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="1dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
android:elevation="1dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:gravity="start|center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:drawableStartCompat="@drawable/mozac_ic_extensions_black"
android:drawablePadding="8dp"
android:text="@string/mozac_extension_install_progress_caption"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/install_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:drawablePadding="8dp"
android:gravity="start|center_vertical"
android:padding="16dp"
android:text="@string/mozac_extension_install_progress_caption"
app:drawableStartCompat="@drawable/mozac_ic_extensions_black" />
<Button
android:id="@+id/cancel_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/install_hint"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/mozac_feature_addons_install_addon_dialog_cancel"
android:textAlignment="center"
android:textAllCaps="false" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
@@ -9,7 +9,7 @@ final _apiInstance = GeckoSuggestionApi();
class GeckoSuggestionsService extends GeckoSuggestionEvents {
final GeckoSuggestionApi _api;
final _suggestionsSubject = BehaviorSubject<List<GeckoSuggestion>>();
final _suggestionsSubject = PublishSubject<List<GeckoSuggestion>>();
Stream<List<GeckoSuggestion>> get suggestionsStream =>
_suggestionsSubject.stream;
@@ -26,7 +26,7 @@ class GeckoSuggestionsService extends GeckoSuggestionEvents {
);
}
Future<void> onInputChanged(
Future<void> querySuggestions(
String text, {
List<GeckoSuggestionType> providers = const [
GeckoSuggestionType.session,
@@ -34,7 +34,11 @@ class GeckoSuggestionsService extends GeckoSuggestionEvents {
GeckoSuggestionType.history,
],
}) {
return _api.onInputChanged(text, providers);
return _api.querySuggestions(text, providers);
}
Future<AutocompleteResult?> getAutocompleteSuggestion(String query) {
return _api.getAutocompleteSuggestion(query);
}
@override
@@ -1273,6 +1273,47 @@ class GeckoEngineSettings {
}
}
class AutocompleteResult {
AutocompleteResult({
required this.input,
required this.text,
required this.url,
required this.source,
required this.totalItems,
});
String input;
String text;
String url;
String source;
int totalItems;
Object encode() {
return <Object?>[
input,
text,
url,
source,
totalItems,
];
}
static AutocompleteResult decode(Object result) {
result as List<Object?>;
return AutocompleteResult(
input: result[0]! as String,
text: result[1]! as String,
url: result[2]! as String,
source: result[3]! as String,
totalItems: result[4]! as int,
);
}
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@@ -1404,6 +1445,9 @@ class _PigeonCodec extends StandardMessageCodec {
} else if (value is GeckoEngineSettings) {
buffer.putUint8(169);
writeValue(buffer, value.encode());
} else if (value is AutocompleteResult) {
buffer.putUint8(170);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@@ -1507,6 +1551,8 @@ class _PigeonCodec extends StandardMessageCodec {
return TabContent.decode(readValue(buffer)!);
case 169:
return GeckoEngineSettings.decode(readValue(buffer)!);
case 170:
return AutocompleteResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
@@ -3501,8 +3547,30 @@ class GeckoSuggestionApi {
final String pigeonVar_messageChannelSuffix;
Future<void> onInputChanged(String text, List<GeckoSuggestionType> providers) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.onInputChanged$pigeonVar_messageChannelSuffix';
Future<AutocompleteResult?> getAutocompleteSuggestion(String query) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.getAutocompleteSuggestion$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_channel.send(<Object?>[query]) as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return (pigeonVar_replyList[0] as AutocompleteResult?);
}
}
Future<void> querySuggestions(String text, List<GeckoSuggestionType> providers) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.querySuggestions$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
@@ -596,6 +596,17 @@ class GeckoEngineSettings {
);
}
class AutocompleteResult {
final String input;
final String text;
final String url;
final String source;
final int totalItems;
AutocompleteResult(
this.input, this.text, this.url, this.source, this.totalItems);
}
// /// Represents all the different supported types of data that can be found from long clicking
// /// an element.
// sealed class HitResult {
@@ -998,7 +1009,9 @@ abstract class GeckoAddonEvents {
@HostApi()
abstract class GeckoSuggestionApi {
void onInputChanged(String text, List<GeckoSuggestionType> providers);
@async
AutocompleteResult? getAutocompleteSuggestion(String query);
void querySuggestions(String text, List<GeckoSuggestionType> providers);
}
@FlutterApi()