feat: tab suggestions
This commit is contained in:
@@ -16,7 +16,7 @@ import 'package:weblibre/features/geckoview/domain/providers.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container_topic.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||||
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
|
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
|
||||||
|
|
||||||
part 'tab_state.g.dart';
|
part 'tab_state.g.dart';
|
||||||
@@ -43,7 +43,7 @@ class TabStates extends _$TabStates {
|
|||||||
|
|
||||||
if (newState.isFinishedLoading) {
|
if (newState.isFinishedLoading) {
|
||||||
ref
|
ref
|
||||||
.read(containerTopicRepositoryProvider.notifier)
|
.read(geckoInferenceRepositoryProvider.notifier)
|
||||||
.markInitialLoadComplete();
|
.markInitialLoadComplete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ final selectedTabContainerIdProvider = Provider<AsyncValue<String?>>.internal(
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef SelectedTabContainerIdRef = ProviderRef<AsyncValue<String?>>;
|
typedef SelectedTabContainerIdRef = ProviderRef<AsyncValue<String?>>;
|
||||||
String _$tabStatesHash() => r'ec7e0905d77b2f82491c5b5af0d2ce1351a564f4';
|
String _$tabStatesHash() => r'66cff6a7b36328ee23b89fdd53046987b346bfcd';
|
||||||
|
|
||||||
/// See also [TabStates].
|
/// See also [TabStates].
|
||||||
@ProviderFor(TabStates)
|
@ProviderFor(TabStates)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fast_equatable/fast_equatable.dart';
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
@@ -11,6 +12,7 @@ import 'package:weblibre/features/geckoview/features/search/domain/entities/tab_
|
|||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
|
|
||||||
part 'providers.g.dart';
|
part 'providers.g.dart';
|
||||||
@@ -88,6 +90,37 @@ EquatableValue<Map<String, TabState>> availableTabStates(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
EquatableValue<List<TabEntity>> suggestedTabEntities(
|
||||||
|
Ref ref,
|
||||||
|
String? containerId,
|
||||||
|
) {
|
||||||
|
final excludedTabIds = ref.watch(
|
||||||
|
containerTabIdsProvider(
|
||||||
|
// ignore: provider_parameters
|
||||||
|
ContainerFilterById(containerId: containerId),
|
||||||
|
).select((value) => EquatableValue(value.valueOrNull)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final suggestions = ref.watch(
|
||||||
|
containerTabSuggestionsProvider(containerId).select(
|
||||||
|
(value) => EquatableValue(
|
||||||
|
value.valueOrNull.mapNotNull(
|
||||||
|
(result) => result
|
||||||
|
.whereNot(
|
||||||
|
(tabId) => excludedTabIds.value?.contains(tabId) ?? false,
|
||||||
|
)
|
||||||
|
.map((tabId) => DefaultTabEntity(tabId: tabId))
|
||||||
|
.toList(),
|
||||||
|
) ??
|
||||||
|
const [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
||||||
Ref ref, {
|
Ref ref, {
|
||||||
|
|||||||
@@ -282,6 +282,134 @@ class _AvailableTabStatesProviderElement
|
|||||||
(origin as AvailableTabStatesProvider).containerFilter;
|
(origin as AvailableTabStatesProvider).containerFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _$suggestedTabEntitiesHash() =>
|
||||||
|
r'f8c41ea3136ae0761e558323afb9d33bf5b8fd21';
|
||||||
|
|
||||||
|
/// See also [suggestedTabEntities].
|
||||||
|
@ProviderFor(suggestedTabEntities)
|
||||||
|
const suggestedTabEntitiesProvider = SuggestedTabEntitiesFamily();
|
||||||
|
|
||||||
|
/// See also [suggestedTabEntities].
|
||||||
|
class SuggestedTabEntitiesFamily
|
||||||
|
extends Family<EquatableValue<List<TabEntity>>> {
|
||||||
|
/// See also [suggestedTabEntities].
|
||||||
|
const SuggestedTabEntitiesFamily();
|
||||||
|
|
||||||
|
/// See also [suggestedTabEntities].
|
||||||
|
SuggestedTabEntitiesProvider call(String? containerId) {
|
||||||
|
return SuggestedTabEntitiesProvider(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SuggestedTabEntitiesProvider getProviderOverride(
|
||||||
|
covariant SuggestedTabEntitiesProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'suggestedTabEntitiesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [suggestedTabEntities].
|
||||||
|
class SuggestedTabEntitiesProvider
|
||||||
|
extends AutoDisposeProvider<EquatableValue<List<TabEntity>>> {
|
||||||
|
/// See also [suggestedTabEntities].
|
||||||
|
SuggestedTabEntitiesProvider(String? containerId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) =>
|
||||||
|
suggestedTabEntities(ref as SuggestedTabEntitiesRef, containerId),
|
||||||
|
from: suggestedTabEntitiesProvider,
|
||||||
|
name: r'suggestedTabEntitiesProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$suggestedTabEntitiesHash,
|
||||||
|
dependencies: SuggestedTabEntitiesFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SuggestedTabEntitiesFamily._allTransitiveDependencies,
|
||||||
|
containerId: containerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
SuggestedTabEntitiesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.containerId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String? containerId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
EquatableValue<List<TabEntity>> Function(SuggestedTabEntitiesRef provider)
|
||||||
|
create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SuggestedTabEntitiesProvider._internal(
|
||||||
|
(ref) => create(ref as SuggestedTabEntitiesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
containerId: containerId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeProviderElement<EquatableValue<List<TabEntity>>> createElement() {
|
||||||
|
return _SuggestedTabEntitiesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SuggestedTabEntitiesProvider &&
|
||||||
|
other.containerId == containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, containerId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SuggestedTabEntitiesRef
|
||||||
|
on AutoDisposeProviderRef<EquatableValue<List<TabEntity>>> {
|
||||||
|
/// The parameter `containerId` of this provider.
|
||||||
|
String? get containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SuggestedTabEntitiesProviderElement
|
||||||
|
extends AutoDisposeProviderElement<EquatableValue<List<TabEntity>>>
|
||||||
|
with SuggestedTabEntitiesRef {
|
||||||
|
_SuggestedTabEntitiesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get containerId =>
|
||||||
|
(origin as SuggestedTabEntitiesProvider).containerId;
|
||||||
|
}
|
||||||
|
|
||||||
String _$seamlessFilteredTabEntitiesHash() =>
|
String _$seamlessFilteredTabEntitiesHash() =>
|
||||||
r'2cd698d4e7c9d9cda9e17abbf9a99e512425e4cd';
|
r'2cd698d4e7c9d9cda9e17abbf9a99e512425e4cd';
|
||||||
|
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'tab_suggestions.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class TabSuggestionsController extends _$TabSuggestionsController {
|
||||||
|
void toggle() {
|
||||||
|
state = !state;
|
||||||
|
}
|
||||||
|
|
||||||
|
void hide() {
|
||||||
|
if (state) {
|
||||||
|
state = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool build() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'tab_suggestions.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$tabSuggestionsControllerHash() =>
|
||||||
|
r'5ce7f385b8aba14d432912cf0acb06aad04433fc';
|
||||||
|
|
||||||
|
/// See also [TabSuggestionsController].
|
||||||
|
@ProviderFor(TabSuggestionsController)
|
||||||
|
final tabSuggestionsControllerProvider =
|
||||||
|
NotifierProvider<TabSuggestionsController, bool>.internal(
|
||||||
|
TabSuggestionsController.new,
|
||||||
|
name: r'tabSuggestionsControllerProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$tabSuggestionsControllerHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$TabSuggestionsController = Notifier<bool>;
|
||||||
|
// 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
|
||||||
+100
-48
@@ -13,6 +13,7 @@ import 'package:weblibre/data/models/drag_data.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_suggestions.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tree_view.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tree_view.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
||||||
@@ -29,9 +30,14 @@ import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
|
|||||||
|
|
||||||
class _TabDraggable extends HookConsumerWidget {
|
class _TabDraggable extends HookConsumerWidget {
|
||||||
final TabEntity entity;
|
final TabEntity entity;
|
||||||
|
final String? suggestedContainerId;
|
||||||
final VoidCallback onClose;
|
final VoidCallback onClose;
|
||||||
|
|
||||||
const _TabDraggable({required this.entity, required this.onClose});
|
const _TabDraggable({
|
||||||
|
required this.entity,
|
||||||
|
required this.onClose,
|
||||||
|
this.suggestedContainerId,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -49,16 +55,22 @@ class _TabDraggable extends HookConsumerWidget {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
final tab = SingleTabPreview(
|
final tab = (suggestedContainerId != null)
|
||||||
tabId: entity.tabId,
|
? SuggestedSingleTabPreview(
|
||||||
activeTabId: activeTab,
|
tabId: entity.tabId,
|
||||||
onClose: onClose,
|
activeTabId: activeTab,
|
||||||
sourceSearchQuery: switch (entity) {
|
containerId: suggestedContainerId!,
|
||||||
DefaultTabEntity _ => null,
|
)
|
||||||
final SearchResultTabEntity entity => entity.searchQuery,
|
: SingleTabPreview(
|
||||||
TabTreeEntity _ => throw UnimplementedError(),
|
tabId: entity.tabId,
|
||||||
},
|
activeTabId: activeTab,
|
||||||
);
|
onClose: onClose,
|
||||||
|
sourceSearchQuery: switch (entity) {
|
||||||
|
DefaultTabEntity _ => null,
|
||||||
|
final SearchResultTabEntity entity => entity.searchQuery,
|
||||||
|
TabTreeEntity _ => throw UnimplementedError(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return switch (dragData) {
|
return switch (dragData) {
|
||||||
ContainerDropData() => Opacity(
|
ContainerDropData() => Opacity(
|
||||||
@@ -132,6 +144,10 @@ class _TabSheetHeader extends HookConsumerWidget {
|
|||||||
searchTextFocus.requestFocus();
|
searchTextFocus.requestFocus();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: VerticalDivider(indent: 4, endIndent: 4),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(MdiIcons.familyTree),
|
icon: const Icon(MdiIcons.familyTree),
|
||||||
selectedIcon: const Icon(MdiIcons.table),
|
selectedIcon: const Icon(MdiIcons.table),
|
||||||
@@ -144,6 +160,27 @@ class _TabSheetHeader extends HookConsumerWidget {
|
|||||||
.toggle();
|
.toggle();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final tabSuggestionsEnabled = ref.watch(
|
||||||
|
tabSuggestionsControllerProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
return IconButton.filledTonal(
|
||||||
|
icon: const Icon(MdiIcons.imageAutoAdjust),
|
||||||
|
isSelected: tabSuggestionsEnabled,
|
||||||
|
iconSize: 18,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabSuggestionsControllerProvider.notifier,
|
||||||
|
)
|
||||||
|
.toggle();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
@@ -298,7 +335,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final gridViewKey = useMemoized(() => GlobalKey());
|
final gridViewKey = useMemoized(() => GlobalKey());
|
||||||
|
|
||||||
final container = ref.watch(selectedContainerProvider);
|
final containerId = ref.watch(selectedContainerProvider);
|
||||||
|
|
||||||
final filteredTabEntities = ref.watch(
|
final filteredTabEntities = ref.watch(
|
||||||
seamlessFilteredTabEntitiesProvider(
|
seamlessFilteredTabEntitiesProvider(
|
||||||
@@ -306,35 +343,36 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
// ignore: document_ignores using fast equatable
|
// ignore: document_ignores using fast equatable
|
||||||
// ignore: provider_parameters
|
// ignore: provider_parameters
|
||||||
containerFilter: ContainerFilterById(
|
containerFilter: ContainerFilterById(
|
||||||
containerId: container,
|
containerId: containerId,
|
||||||
),
|
),
|
||||||
groupTrees: false,
|
groupTrees: false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final tabSuggestionsEnabled = ref.watch(
|
||||||
|
tabSuggestionsControllerProvider,
|
||||||
|
);
|
||||||
|
final suggestedTabEntities = ref.watch(
|
||||||
|
suggestedTabEntitiesProvider(
|
||||||
|
tabSuggestionsEnabled ? containerId : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final itemCount =
|
||||||
|
filteredTabEntities.value.length +
|
||||||
|
suggestedTabEntities.value.length;
|
||||||
|
|
||||||
final activeTab = ref.watch(selectedTabProvider);
|
final activeTab = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
final crossAxisCount = useMemoized(
|
final crossAxisCount = useMemoized(() {
|
||||||
() {
|
final calculatedCount = _calculateCrossAxisItemCount(
|
||||||
final calculatedCount = _calculateCrossAxisItemCount(
|
screenWidth: MediaQuery.of(context).size.width,
|
||||||
screenWidth: MediaQuery.of(context).size.width,
|
horizontalPadding: 4.0,
|
||||||
horizontalPadding: 4.0,
|
crossAxisSpacing: 8.0,
|
||||||
crossAxisSpacing: 8.0,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return math.max(
|
return math.max(math.min(calculatedCount, itemCount), 2);
|
||||||
math.min(
|
}, [MediaQuery.of(context).size.width, itemCount]);
|
||||||
calculatedCount,
|
|
||||||
filteredTabEntities.value.length,
|
|
||||||
),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[
|
|
||||||
MediaQuery.of(context).size.width,
|
|
||||||
filteredTabEntities.value.length,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
final itemHeight = useMemoized(
|
final itemHeight = useMemoized(
|
||||||
() => _calculateItemHeight(
|
() => _calculateItemHeight(
|
||||||
@@ -390,20 +428,29 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final tabs = useMemoized(() {
|
final tabs = useMemoized(() {
|
||||||
return filteredTabEntities.value
|
return [
|
||||||
.where((entity) => entity is! TabTreeEntity)
|
...filteredTabEntities.value.map(
|
||||||
.map(
|
(entity) => CustomDraggable(
|
||||||
(entity) => CustomDraggable(
|
key: Key(entity.tabId),
|
||||||
key: Key(entity.tabId),
|
data: TabDragData(entity.tabId),
|
||||||
data: TabDragData(entity.tabId),
|
child: _TabDraggable(
|
||||||
child: _TabDraggable(
|
entity: entity,
|
||||||
entity: entity,
|
onClose: onClose,
|
||||||
onClose: onClose,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
.toList();
|
),
|
||||||
}, [filteredTabEntities]);
|
...suggestedTabEntities.value.map(
|
||||||
|
(entity) => CustomDraggable(
|
||||||
|
key: Key('suggested_${entity.tabId}'),
|
||||||
|
child: _TabDraggable(
|
||||||
|
entity: entity,
|
||||||
|
onClose: onClose,
|
||||||
|
suggestedContainerId: containerId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}, [filteredTabEntities, suggestedTabEntities]);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
@@ -415,7 +462,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
//Rebuild when cross axis count changes
|
//Rebuild when cross axis count changes
|
||||||
key: ValueKey(crossAxisCount),
|
key: ValueKey(crossAxisCount),
|
||||||
scrollController: controller,
|
scrollController: controller,
|
||||||
itemCount: tabs.length,
|
itemCount: itemCount,
|
||||||
onDragStarted: (index) {
|
onDragStarted: (index) {
|
||||||
ref.read(willAcceptDropProvider.notifier).clear();
|
ref.read(willAcceptDropProvider.notifier).clear();
|
||||||
},
|
},
|
||||||
@@ -432,6 +479,11 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
containerRepositoryProvider.notifier,
|
containerRepositoryProvider.notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Suggestions are at the end and not reorderable, so skip
|
||||||
|
if (oldIndex >= filteredTabEntities.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final tabId =
|
final tabId =
|
||||||
filteredTabEntities.value[oldIndex].tabId;
|
filteredTabEntities.value[oldIndex].tabId;
|
||||||
final containerId = await ref
|
final containerId = await ref
|
||||||
@@ -482,7 +534,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
crossAxisSpacing: 8.0,
|
crossAxisSpacing: 8.0,
|
||||||
crossAxisCount: crossAxisCount,
|
crossAxisCount: crossAxisCount,
|
||||||
),
|
),
|
||||||
itemCount: tabs.length,
|
itemCount: itemCount,
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, index) =>
|
||||||
itemBuilder(tabs[index], index),
|
itemBuilder(tabs[index], index),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
@@ -14,15 +15,21 @@ import 'package:weblibre/features/geckoview/features/find_in_page/domain/entitie
|
|||||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
|
|
||||||
class _TabBox extends StatelessWidget {
|
class TabContainer extends StatelessWidget {
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final bool isPrivate;
|
final bool isPrivate;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
const _TabBox({required this.isActive, required this.isPrivate, this.child});
|
const TabContainer({
|
||||||
|
required this.isActive,
|
||||||
|
required this.isPrivate,
|
||||||
|
this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -47,6 +54,37 @@ class _TabBox extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TabMiniPreview extends HookConsumerWidget {
|
||||||
|
final String tabId;
|
||||||
|
|
||||||
|
const TabMiniPreview({super.key, required this.tabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final thumbnail = ref.watch(
|
||||||
|
tabStateProvider(tabId).select((value) => value?.thumbnail),
|
||||||
|
);
|
||||||
|
|
||||||
|
return TabContainer(
|
||||||
|
isActive: false,
|
||||||
|
isPrivate: false,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
||||||
|
child: Skeleton.replace(
|
||||||
|
replace: thumbnail == null,
|
||||||
|
replacement: const Bone.square(size: double.infinity),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: RawImage(image: thumbnail?.value, fit: BoxFit.fitWidth),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TabPreview extends HookWidget {
|
class TabPreview extends HookWidget {
|
||||||
final TabState tab;
|
final TabState tab;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
@@ -57,6 +95,8 @@ class TabPreview extends HookWidget {
|
|||||||
final VoidCallback? onDelete;
|
final VoidCallback? onDelete;
|
||||||
final void Function(String host)? onDeleteAll;
|
final void Function(String host)? onDeleteAll;
|
||||||
|
|
||||||
|
final Widget? trailingChild;
|
||||||
|
|
||||||
const TabPreview({
|
const TabPreview({
|
||||||
required this.tab,
|
required this.tab,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
@@ -65,13 +105,14 @@ class TabPreview extends HookWidget {
|
|||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
this.onDeleteAll,
|
this.onDeleteAll,
|
||||||
|
this.trailingChild,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final extendedDeleteMenuController = useMenuController();
|
final extendedDeleteMenuController = useMenuController();
|
||||||
|
|
||||||
return _TabBox(
|
return TabContainer(
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
isPrivate: tab.isPrivate,
|
isPrivate: tab.isPrivate,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -130,6 +171,7 @@ class TabPreview extends HookWidget {
|
|||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
?trailingChild,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -259,6 +301,51 @@ class SingleTabPreview extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SuggestedSingleTabPreview extends HookConsumerWidget {
|
||||||
|
final String tabId;
|
||||||
|
final String containerId;
|
||||||
|
|
||||||
|
final String? activeTabId;
|
||||||
|
|
||||||
|
SuggestedSingleTabPreview({
|
||||||
|
required this.tabId,
|
||||||
|
required this.containerId,
|
||||||
|
required this.activeTabId,
|
||||||
|
}) : super(key: ValueKey(tabId));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tab = ref.watch(tabStateProvider(tabId));
|
||||||
|
|
||||||
|
if (tab == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: TabPreview(
|
||||||
|
tab: tab,
|
||||||
|
isActive: tabId == activeTabId,
|
||||||
|
onTap: () async {
|
||||||
|
final containerData = await ref
|
||||||
|
.read(containerRepositoryProvider.notifier)
|
||||||
|
.getContainerData(containerId);
|
||||||
|
|
||||||
|
if (containerData != null) {
|
||||||
|
await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.assignContainer(tabId, containerData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingChild: const IconButton(
|
||||||
|
icon: Icon(MdiIcons.creation),
|
||||||
|
onPressed: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TabTreePreview extends HookConsumerWidget {
|
class TabTreePreview extends HookConsumerWidget {
|
||||||
final TabTreeEntity entity;
|
final TabTreeEntity entity;
|
||||||
final String? activeTabId;
|
final String? activeTabId;
|
||||||
@@ -303,7 +390,7 @@ class TabTreePreview extends HookConsumerWidget {
|
|||||||
_addPadding(
|
_addPadding(
|
||||||
index,
|
index,
|
||||||
stackCount,
|
stackCount,
|
||||||
_TabBox(
|
TabContainer(
|
||||||
isActive: entity.tabId == activeTabId,
|
isActive: entity.tabId == activeTabId,
|
||||||
isPrivate: tab.isPrivate,
|
isPrivate: tab.isPrivate,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -73,11 +73,17 @@ Stream<Map<String, String?>> tabDescendants(Ref ref, String tabId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Stream<List<TabData>> containerTabsData(Ref ref, String containerId) {
|
Stream<List<TabData>> containerTabsData(Ref ref, String? containerId) {
|
||||||
final db = ref.watch(tabDatabaseProvider);
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
return db.containerDao.getContainerTabsData(containerId).watch();
|
return db.containerDao.getContainerTabsData(containerId).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Stream<ContainerData?> containerData(Ref ref, String containerId) {
|
||||||
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
return db.containerDao.getContainerData(containerId).watchSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Stream<String?> watchContainerTabId(Ref ref, String tabId) {
|
Stream<String?> watchContainerTabId(Ref ref, String tabId) {
|
||||||
return ref
|
return ref
|
||||||
|
|||||||
@@ -565,7 +565,7 @@ class _TabDescendantsProviderElement
|
|||||||
String get tabId => (origin as TabDescendantsProvider).tabId;
|
String get tabId => (origin as TabDescendantsProvider).tabId;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$containerTabsDataHash() => r'1987b2d69f2ba663a7343e93572aad3c31a29be3';
|
String _$containerTabsDataHash() => r'2961c89fc9e16c8e6005342356ecf677fb2ff63c';
|
||||||
|
|
||||||
/// See also [containerTabsData].
|
/// See also [containerTabsData].
|
||||||
@ProviderFor(containerTabsData)
|
@ProviderFor(containerTabsData)
|
||||||
@@ -577,7 +577,7 @@ class ContainerTabsDataFamily extends Family<AsyncValue<List<TabData>>> {
|
|||||||
const ContainerTabsDataFamily();
|
const ContainerTabsDataFamily();
|
||||||
|
|
||||||
/// See also [containerTabsData].
|
/// See also [containerTabsData].
|
||||||
ContainerTabsDataProvider call(String containerId) {
|
ContainerTabsDataProvider call(String? containerId) {
|
||||||
return ContainerTabsDataProvider(containerId);
|
return ContainerTabsDataProvider(containerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,7 +607,7 @@ class ContainerTabsDataFamily extends Family<AsyncValue<List<TabData>>> {
|
|||||||
class ContainerTabsDataProvider
|
class ContainerTabsDataProvider
|
||||||
extends AutoDisposeStreamProvider<List<TabData>> {
|
extends AutoDisposeStreamProvider<List<TabData>> {
|
||||||
/// See also [containerTabsData].
|
/// See also [containerTabsData].
|
||||||
ContainerTabsDataProvider(String containerId)
|
ContainerTabsDataProvider(String? containerId)
|
||||||
: this._internal(
|
: this._internal(
|
||||||
(ref) => containerTabsData(ref as ContainerTabsDataRef, containerId),
|
(ref) => containerTabsData(ref as ContainerTabsDataRef, containerId),
|
||||||
from: containerTabsDataProvider,
|
from: containerTabsDataProvider,
|
||||||
@@ -631,7 +631,7 @@ class ContainerTabsDataProvider
|
|||||||
required this.containerId,
|
required this.containerId,
|
||||||
}) : super.internal();
|
}) : super.internal();
|
||||||
|
|
||||||
final String containerId;
|
final String? containerId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Override overrideWith(
|
Override overrideWith(
|
||||||
@@ -675,7 +675,7 @@ class ContainerTabsDataProvider
|
|||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
mixin ContainerTabsDataRef on AutoDisposeStreamProviderRef<List<TabData>> {
|
mixin ContainerTabsDataRef on AutoDisposeStreamProviderRef<List<TabData>> {
|
||||||
/// The parameter `containerId` of this provider.
|
/// The parameter `containerId` of this provider.
|
||||||
String get containerId;
|
String? get containerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContainerTabsDataProviderElement
|
class _ContainerTabsDataProviderElement
|
||||||
@@ -684,7 +684,127 @@ class _ContainerTabsDataProviderElement
|
|||||||
_ContainerTabsDataProviderElement(super.provider);
|
_ContainerTabsDataProviderElement(super.provider);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get containerId => (origin as ContainerTabsDataProvider).containerId;
|
String? get containerId => (origin as ContainerTabsDataProvider).containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$containerDataHash() => r'6adbec128876069189954832af35109b0779148c';
|
||||||
|
|
||||||
|
/// See also [containerData].
|
||||||
|
@ProviderFor(containerData)
|
||||||
|
const containerDataProvider = ContainerDataFamily();
|
||||||
|
|
||||||
|
/// See also [containerData].
|
||||||
|
class ContainerDataFamily extends Family<AsyncValue<ContainerData?>> {
|
||||||
|
/// See also [containerData].
|
||||||
|
const ContainerDataFamily();
|
||||||
|
|
||||||
|
/// See also [containerData].
|
||||||
|
ContainerDataProvider call(String containerId) {
|
||||||
|
return ContainerDataProvider(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ContainerDataProvider getProviderOverride(
|
||||||
|
covariant ContainerDataProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'containerDataProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [containerData].
|
||||||
|
class ContainerDataProvider extends AutoDisposeStreamProvider<ContainerData?> {
|
||||||
|
/// See also [containerData].
|
||||||
|
ContainerDataProvider(String containerId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => containerData(ref as ContainerDataRef, containerId),
|
||||||
|
from: containerDataProvider,
|
||||||
|
name: r'containerDataProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$containerDataHash,
|
||||||
|
dependencies: ContainerDataFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
ContainerDataFamily._allTransitiveDependencies,
|
||||||
|
containerId: containerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
ContainerDataProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.containerId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String containerId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
Stream<ContainerData?> Function(ContainerDataRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: ContainerDataProvider._internal(
|
||||||
|
(ref) => create(ref as ContainerDataRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
containerId: containerId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeStreamProviderElement<ContainerData?> createElement() {
|
||||||
|
return _ContainerDataProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is ContainerDataProvider && other.containerId == containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, containerId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin ContainerDataRef on AutoDisposeStreamProviderRef<ContainerData?> {
|
||||||
|
/// The parameter `containerId` of this provider.
|
||||||
|
String get containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContainerDataProviderElement
|
||||||
|
extends AutoDisposeStreamProviderElement<ContainerData?>
|
||||||
|
with ContainerDataRef {
|
||||||
|
_ContainerDataProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get containerId => (origin as ContainerDataProvider).containerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$watchContainerTabIdHash() =>
|
String _$watchContainerTabIdHash() =>
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fast_equatable/fast_equatable.dart';
|
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
|
||||||
import 'package:riverpod/riverpod.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
import 'package:synchronized/synchronized.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
|
||||||
import 'package:weblibre/utils/lru_cache.dart';
|
|
||||||
|
|
||||||
part 'container_topic.g.dart';
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
class ContainerTopicRepository extends _$ContainerTopicRepository {
|
|
||||||
final _service = GeckoMlService();
|
|
||||||
|
|
||||||
//Wait for first complete page laod of any website after startup to ensure everything is ready
|
|
||||||
final _initialLoadComplete = Completer();
|
|
||||||
final _lock = Lock();
|
|
||||||
|
|
||||||
final _cache = LRUCache<Set<String>, String>(
|
|
||||||
50,
|
|
||||||
equals: (a, b) {
|
|
||||||
return const DeepCollectionEquality.unordered().equals(a, b);
|
|
||||||
},
|
|
||||||
hashCode: (key) {
|
|
||||||
return const DeepCollectionEquality.unordered().hash(key);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
void markInitialLoadComplete() {
|
|
||||||
if (!_initialLoadComplete.isCompleted) {
|
|
||||||
_initialLoadComplete.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> getContainerTopic(Set<String> titles) async {
|
|
||||||
if (titles.isNotEmpty) {
|
|
||||||
if (_cache.get(titles) case final String title) {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final title = await _lock.synchronized(() async {
|
|
||||||
await _initialLoadComplete.future;
|
|
||||||
|
|
||||||
final title = await _service.getContainerTopic(titles);
|
|
||||||
|
|
||||||
return _cache.set(titles, title);
|
|
||||||
}, timeout: const Duration(seconds: 120));
|
|
||||||
|
|
||||||
return title;
|
|
||||||
} on TimeoutException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void build() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
Future<String?> containerTopic(Ref ref, String containerId) async {
|
|
||||||
final titles = await ref.watch(
|
|
||||||
containerTabsDataProvider(containerId).selectAsync(
|
|
||||||
(tabData) =>
|
|
||||||
EquatableValue(tabData.map((tab) => tab.title).nonNulls.toSet()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final topic = await ref
|
|
||||||
.read(containerTopicRepositoryProvider.notifier)
|
|
||||||
.getContainerTopic(titles.value);
|
|
||||||
|
|
||||||
return topic;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
|
import 'package:nullability/nullability.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/utils/embedding_text_processing.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/utils/nearest_neighbor.dart';
|
||||||
|
import 'package:weblibre/utils/lru_cache.dart';
|
||||||
|
|
||||||
|
part 'gecko_inference.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class GeckoInferenceRepository extends _$GeckoInferenceRepository {
|
||||||
|
final _service = GeckoMlService();
|
||||||
|
|
||||||
|
//Wait for first complete page laod of any website after startup to ensure everything is ready
|
||||||
|
final _initialLoadComplete = Completer();
|
||||||
|
final _engineLock = Lock();
|
||||||
|
|
||||||
|
final _topicCache = LRUCache<Set<String>, String>(
|
||||||
|
50,
|
||||||
|
equals: (a, b) {
|
||||||
|
return const DeepCollectionEquality.unordered().equals(a, b);
|
||||||
|
},
|
||||||
|
hashCode: (key) {
|
||||||
|
return const DeepCollectionEquality.unordered().hash(key);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final _embeddingCache = LRUCache<String, List<double>>(100);
|
||||||
|
|
||||||
|
void markInitialLoadComplete() {
|
||||||
|
if (!_initialLoadComplete.isCompleted) {
|
||||||
|
_initialLoadComplete.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> predictDocumentTopic(Set<String> titles) async {
|
||||||
|
if (titles.isNotEmpty) {
|
||||||
|
if (_topicCache.get(titles) case final String title) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final title = await _engineLock.synchronized(() async {
|
||||||
|
await _initialLoadComplete.future;
|
||||||
|
|
||||||
|
final title = await _service.predictDocumentTopic(titles);
|
||||||
|
|
||||||
|
return _topicCache.set(titles, title);
|
||||||
|
}, timeout: const Duration(seconds: 120));
|
||||||
|
|
||||||
|
return title;
|
||||||
|
} on TimeoutException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>?> suggestDocuments({
|
||||||
|
required String topic,
|
||||||
|
required List<String> assignedDocumentsInput,
|
||||||
|
required List<String> unassignedDocumentsInput,
|
||||||
|
}) async {
|
||||||
|
final processedDocuments = <String, String>{};
|
||||||
|
final unassignedDocumentsProcessed = unassignedDocumentsInput.map((doc) {
|
||||||
|
final processed = preprocessText(doc);
|
||||||
|
if (processed != doc) {
|
||||||
|
processedDocuments[processed] = doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final assignedDocumentsProcessed = assignedDocumentsInput
|
||||||
|
.map((doc) => '$topic. ${preprocessText(doc)}')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final embeddings = await generateDocumentEmbeddings([
|
||||||
|
...unassignedDocumentsProcessed,
|
||||||
|
...assignedDocumentsProcessed,
|
||||||
|
]);
|
||||||
|
|
||||||
|
final neighbors = embeddings.mapNotNull(
|
||||||
|
(embeddings) => findNearestNeighborsRecursive(
|
||||||
|
embeddings: embeddings,
|
||||||
|
assignedDocuments: assignedDocumentsProcessed,
|
||||||
|
unassignedDocuments: unassignedDocumentsProcessed,
|
||||||
|
).map((neighbor) => processedDocuments[neighbor] ?? neighbor).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return neighbors;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, List<double>>?> generateDocumentEmbeddings(
|
||||||
|
List<String> documents,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final embeddings = Map.fromEntries(
|
||||||
|
documents.map((doc) => MapEntry(doc, _embeddingCache.get(doc))),
|
||||||
|
);
|
||||||
|
|
||||||
|
final embeddingsToGenerate = embeddings.entries
|
||||||
|
.where((e) => e.value == null)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (embeddingsToGenerate.isNotEmpty) {
|
||||||
|
final generatedEmbeddings = await _engineLock.synchronized(() async {
|
||||||
|
await _initialLoadComplete.future;
|
||||||
|
|
||||||
|
final embeddings = await _service.generateDocumentEmbeddings(
|
||||||
|
documents,
|
||||||
|
);
|
||||||
|
|
||||||
|
return embeddings;
|
||||||
|
}, timeout: const Duration(seconds: 120));
|
||||||
|
|
||||||
|
for (var i = 0; i < embeddingsToGenerate.length; i++) {
|
||||||
|
embeddings[embeddingsToGenerate[i]] = generatedEmbeddings[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
for (final MapEntry(:key, :value) in embeddings.entries)
|
||||||
|
if (value != null) key: value,
|
||||||
|
};
|
||||||
|
} on TimeoutException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Future<String?> containerTopic(Ref ref, String containerId) async {
|
||||||
|
final titles = await ref.watch(
|
||||||
|
containerTabsDataProvider(containerId).selectAsync(
|
||||||
|
(tabData) =>
|
||||||
|
EquatableValue(tabData.map((tab) => tab.title).nonNulls.toSet()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final topic = await ref
|
||||||
|
.read(geckoInferenceRepositoryProvider.notifier)
|
||||||
|
.predictDocumentTopic(titles.value);
|
||||||
|
|
||||||
|
return topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Future<List<String>?> containerTabSuggestions(
|
||||||
|
Ref ref,
|
||||||
|
String? containerId,
|
||||||
|
) async {
|
||||||
|
if (containerId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final container = await ref.watch(containerDataProvider(containerId).future);
|
||||||
|
|
||||||
|
final assignedTitles = await ref.watch(
|
||||||
|
containerTabsDataProvider(containerId).selectAsync(
|
||||||
|
(tabData) => EquatableValue(
|
||||||
|
tabData
|
||||||
|
.where((tab) => tab.title.isNotEmpty)
|
||||||
|
.map((tab) => (tab.id, tab.title!))
|
||||||
|
.toSet(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final unassignedTitles = await ref.watch(
|
||||||
|
containerTabsDataProvider(null).selectAsync(
|
||||||
|
(tabData) => EquatableValue(
|
||||||
|
tabData
|
||||||
|
.where((tab) => tab.title.isNotEmpty)
|
||||||
|
.map((tab) => (tab.id, tab.title!))
|
||||||
|
.toSet(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (assignedTitles.value.isNotEmpty && unassignedTitles.value.isNotEmpty) {
|
||||||
|
final topic =
|
||||||
|
container?.name ??
|
||||||
|
await ref
|
||||||
|
.read(geckoInferenceRepositoryProvider.notifier)
|
||||||
|
.predictDocumentTopic(
|
||||||
|
assignedTitles.value.map((tab) => tab.$2).toSet(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (topic != null) {
|
||||||
|
final suggestedTitles = await ref
|
||||||
|
.read(geckoInferenceRepositoryProvider.notifier)
|
||||||
|
.suggestDocuments(
|
||||||
|
topic: topic,
|
||||||
|
assignedDocumentsInput: assignedTitles.value
|
||||||
|
.map((tab) => tab.$2)
|
||||||
|
.toList(),
|
||||||
|
unassignedDocumentsInput: unassignedTitles.value
|
||||||
|
.map((tab) => tab.$2)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return suggestedTitles.mapNotNull(
|
||||||
|
(titles) => titles
|
||||||
|
.map(
|
||||||
|
(title) => unassignedTitles.value
|
||||||
|
.firstWhere((tab) => tab.$2 == title)
|
||||||
|
.$1,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
+141
-12
@@ -1,12 +1,12 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'container_topic.dart';
|
part of 'gecko_inference.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$containerTopicHash() => r'aa3fd27f26bb94b6c9e794d8470537240b8d2713';
|
String _$containerTopicHash() => r'b680381eab3dded9ca6f174fbcc5da5c59d7e9f0';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@@ -146,22 +146,151 @@ class _ContainerTopicProviderElement extends FutureProviderElement<String?>
|
|||||||
String get containerId => (origin as ContainerTopicProvider).containerId;
|
String get containerId => (origin as ContainerTopicProvider).containerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$containerTopicRepositoryHash() =>
|
String _$containerTabSuggestionsHash() =>
|
||||||
r'8dfc0bc8b4953b5230a1faa24c8b2f0a94ab1f04';
|
r'ab8e596f0c7e8562bf998c701145e48eb1e520d5';
|
||||||
|
|
||||||
/// See also [ContainerTopicRepository].
|
/// See also [containerTabSuggestions].
|
||||||
@ProviderFor(ContainerTopicRepository)
|
@ProviderFor(containerTabSuggestions)
|
||||||
final containerTopicRepositoryProvider =
|
const containerTabSuggestionsProvider = ContainerTabSuggestionsFamily();
|
||||||
NotifierProvider<ContainerTopicRepository, void>.internal(
|
|
||||||
ContainerTopicRepository.new,
|
/// See also [containerTabSuggestions].
|
||||||
name: r'containerTopicRepositoryProvider',
|
class ContainerTabSuggestionsFamily extends Family<AsyncValue<List<String>?>> {
|
||||||
|
/// See also [containerTabSuggestions].
|
||||||
|
const ContainerTabSuggestionsFamily();
|
||||||
|
|
||||||
|
/// See also [containerTabSuggestions].
|
||||||
|
ContainerTabSuggestionsProvider call(String? containerId) {
|
||||||
|
return ContainerTabSuggestionsProvider(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ContainerTabSuggestionsProvider getProviderOverride(
|
||||||
|
covariant ContainerTabSuggestionsProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'containerTabSuggestionsProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [containerTabSuggestions].
|
||||||
|
class ContainerTabSuggestionsProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<String>?> {
|
||||||
|
/// See also [containerTabSuggestions].
|
||||||
|
ContainerTabSuggestionsProvider(String? containerId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => containerTabSuggestions(
|
||||||
|
ref as ContainerTabSuggestionsRef,
|
||||||
|
containerId,
|
||||||
|
),
|
||||||
|
from: containerTabSuggestionsProvider,
|
||||||
|
name: r'containerTabSuggestionsProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$containerTabSuggestionsHash,
|
||||||
|
dependencies: ContainerTabSuggestionsFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
ContainerTabSuggestionsFamily._allTransitiveDependencies,
|
||||||
|
containerId: containerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
ContainerTabSuggestionsProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.containerId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String? containerId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<String>?> Function(ContainerTabSuggestionsRef provider)
|
||||||
|
create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: ContainerTabSuggestionsProvider._internal(
|
||||||
|
(ref) => create(ref as ContainerTabSuggestionsRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
containerId: containerId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<String>?> createElement() {
|
||||||
|
return _ContainerTabSuggestionsProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is ContainerTabSuggestionsProvider &&
|
||||||
|
other.containerId == containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, containerId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin ContainerTabSuggestionsRef
|
||||||
|
on AutoDisposeFutureProviderRef<List<String>?> {
|
||||||
|
/// The parameter `containerId` of this provider.
|
||||||
|
String? get containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContainerTabSuggestionsProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<String>?>
|
||||||
|
with ContainerTabSuggestionsRef {
|
||||||
|
_ContainerTabSuggestionsProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get containerId =>
|
||||||
|
(origin as ContainerTabSuggestionsProvider).containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$geckoInferenceRepositoryHash() =>
|
||||||
|
r'bdae6dab14c33c6503f37f2e17b8d50fb820ac26';
|
||||||
|
|
||||||
|
/// See also [GeckoInferenceRepository].
|
||||||
|
@ProviderFor(GeckoInferenceRepository)
|
||||||
|
final geckoInferenceRepositoryProvider =
|
||||||
|
NotifierProvider<GeckoInferenceRepository, void>.internal(
|
||||||
|
GeckoInferenceRepository.new,
|
||||||
|
name: r'geckoInferenceRepositoryProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
? null
|
? null
|
||||||
: _$containerTopicRepositoryHash,
|
: _$geckoInferenceRepositoryHash,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$ContainerTopicRepository = Notifier<void>;
|
typedef _$GeckoInferenceRepository = Notifier<void>;
|
||||||
// ignore_for_file: type=lint
|
// 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
|
// 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
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container_topic.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||||
|
|
||||||
part 'container_topic.g.dart';
|
part 'container_topic.g.dart';
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
class ContainerTopicController extends _$ContainerTopicController {
|
class ContainerTopicController extends _$ContainerTopicController {
|
||||||
Future<String?> getContainerTopic(String containerId) async {
|
Future<String?> predictDocumentTopic(String containerId) async {
|
||||||
state = const AsyncLoading();
|
state = const AsyncLoading();
|
||||||
|
|
||||||
final result = await AsyncValue.guard(() async {
|
final result = await AsyncValue.guard(() async {
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ part of 'container_topic.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$containerTopicControllerHash() =>
|
String _$containerTopicControllerHash() =>
|
||||||
r'229709202edecb21b20432f8e4d0d7aee293f623';
|
r'fa38ebd3c1352b471e58444b04d1cb0528aae1dd';
|
||||||
|
|
||||||
/// See also [ContainerTopicController].
|
/// See also [ContainerTopicController].
|
||||||
@ProviderFor(ContainerTopicController)
|
@ProviderFor(ContainerTopicController)
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
containerTopicControllerProvider
|
containerTopicControllerProvider
|
||||||
.notifier,
|
.notifier,
|
||||||
)
|
)
|
||||||
.getContainerTopic(
|
.predictDocumentTopic(
|
||||||
initialContainer.id,
|
initialContainer.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:weblibre/core/logger.dart';
|
|||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container_topic.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||||
|
|
||||||
class ContainerTitle extends HookConsumerWidget {
|
class ContainerTitle extends HookConsumerWidget {
|
||||||
final ContainerData container;
|
final ContainerData container;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
/// Calculates cosine similarity between two lists of floats
|
||||||
|
/// The lists don't need to be normalized
|
||||||
|
///
|
||||||
|
/// [a] first list
|
||||||
|
/// [b] second list
|
||||||
|
/// Returns cosine similarity value
|
||||||
|
double cosSim(List<double> a, List<double> b) {
|
||||||
|
if (a.length != b.length) {
|
||||||
|
throw ArgumentError("Lists should have same lengths");
|
||||||
|
}
|
||||||
|
if (a.isEmpty) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double dotProduct = 0;
|
||||||
|
double mA = 0;
|
||||||
|
double mB = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < a.length; i++) {
|
||||||
|
dotProduct += a[i] * b[i];
|
||||||
|
mA += a[i] * a[i];
|
||||||
|
mB += b[i] * b[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
mA = sqrt(mA);
|
||||||
|
mB = sqrt(mB);
|
||||||
|
|
||||||
|
return mA == 0 || mB == 0 ? 0 : dotProduct / (mA * mB);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/// Removes trailing domain-related text such as '... - Mail' or '... | News'
|
||||||
|
/// If there's not enough information remaining after, we keep the text as is
|
||||||
|
/// [text] tab title with potential domain information
|
||||||
|
/// Returns the processed string
|
||||||
|
String preprocessText(String text) {
|
||||||
|
// Matches 'xyz - Domain' or 'xyz | Domain'
|
||||||
|
// with a space before and after delimiter
|
||||||
|
// or if there are multiple delimiters next to each other
|
||||||
|
final delimiters = RegExp(r'(?<=\s)[|–-]+(?=\s)');
|
||||||
|
final splitText = text.split(delimiters);
|
||||||
|
|
||||||
|
// ensure there's enough info without the last element
|
||||||
|
final hasEnoughInfo =
|
||||||
|
splitText.isNotEmpty &&
|
||||||
|
splitText.sublist(0, splitText.length - 1).join(' ').length > 5;
|
||||||
|
|
||||||
|
// domain related texts are usually shorter, this takes care of the most common cases
|
||||||
|
final isPotentialDomainInfo =
|
||||||
|
splitText.length > 1 && splitText.last.length < 20;
|
||||||
|
|
||||||
|
// If both conditions are met, remove the last chunk, filter out empty strings,
|
||||||
|
// join on space, trim, and lowercase
|
||||||
|
if (hasEnoughInfo && isPotentialDomainInfo) {
|
||||||
|
return splitText
|
||||||
|
.sublist(0, splitText.length - 1) // everything except the last element
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.where((t) => t.isNotEmpty) // remove empty strings
|
||||||
|
.join(' ') // join with spaces
|
||||||
|
.trim(); // remove leading/trailing spaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, just return the text
|
||||||
|
return text;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/utils/cosine_similarity.dart';
|
||||||
|
|
||||||
|
List<String> findNearestNeighborsRecursive({
|
||||||
|
required Map<String, List<double>> embeddings,
|
||||||
|
required List<String> assignedDocuments,
|
||||||
|
required List<String> unassignedDocuments,
|
||||||
|
int thresholdMills = 275,
|
||||||
|
int maxAssignedCount = 4,
|
||||||
|
int depth = 0,
|
||||||
|
}) {
|
||||||
|
final closestTabs = <(String, double)>[];
|
||||||
|
final similarTabsIndices = <String>[];
|
||||||
|
|
||||||
|
for (final unassigned in unassignedDocuments) {
|
||||||
|
double? closestScore;
|
||||||
|
for (final assigned in assignedDocuments.take(maxAssignedCount)) {
|
||||||
|
final cosineSim = cosSim(embeddings[unassigned]!, embeddings[assigned]!);
|
||||||
|
|
||||||
|
if (closestScore == null || cosineSim > closestScore) {
|
||||||
|
closestScore = cosineSim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// threshold could also be set via a nimbus experiment, in which case
|
||||||
|
// it will be an int <= 1000
|
||||||
|
if (closestScore != null && closestScore > thresholdMills / 1000) {
|
||||||
|
closestTabs.add((unassigned, closestScore));
|
||||||
|
similarTabsIndices.add(unassigned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closestTabs.sort((a, b) => b.$2.compareTo(a.$2));
|
||||||
|
|
||||||
|
final result = closestTabs.map((t) => t.$1).toList();
|
||||||
|
|
||||||
|
// recurse once if the initial call only had a single tab
|
||||||
|
// and we found at least 1 similar tab - this improves recall
|
||||||
|
if (assignedDocuments.length == 1 && closestTabs.isNotEmpty && depth == 1) {
|
||||||
|
final recurseSimilarTabs = findNearestNeighborsRecursive(
|
||||||
|
unassignedDocuments: unassignedDocuments
|
||||||
|
.whereNot(similarTabsIndices.contains)
|
||||||
|
.toList(),
|
||||||
|
assignedDocuments: similarTabsIndices,
|
||||||
|
thresholdMills: thresholdMills,
|
||||||
|
embeddings: embeddings,
|
||||||
|
depth: depth - 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.addAll(recurseSimilarTabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -96,6 +96,8 @@ dev_dependencies:
|
|||||||
drift_dev: ^2.28.0
|
drift_dev: ^2.28.0
|
||||||
fast_equatable_lint: ^0.3.2
|
fast_equatable_lint: ^0.3.2
|
||||||
flutter_launcher_icons: ^0.14.4
|
flutter_launcher_icons: ^0.14.4
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
go_router_builder: ^3.0.1
|
go_router_builder: ^3.0.1
|
||||||
json_serializable: ^6.9.5
|
json_serializable: ^6.9.5
|
||||||
lint: ^2.8.0
|
lint: ^2.8.0
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/utils/embedding_text_processing.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Text processing basic cases', () {
|
||||||
|
test('trailing domain-like text should be removed', () {
|
||||||
|
expect(
|
||||||
|
preprocessText("Some Title - Random Mail"),
|
||||||
|
equals("Some Title"),
|
||||||
|
reason: "Should remove '- Random Mail' suffix",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trailing domain-like text with |', () {
|
||||||
|
expect(
|
||||||
|
preprocessText("Another Title | Some Video Website"),
|
||||||
|
equals("Another Title"),
|
||||||
|
reason: "Should remove '| Some Video Website' suffix",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no delimiter', () {
|
||||||
|
expect(
|
||||||
|
preprocessText("Simple Title"),
|
||||||
|
equals("Simple Title"),
|
||||||
|
reason: "Should remain unchanged since there's no recognized delimiter",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('not enough info in first part', () {
|
||||||
|
expect(
|
||||||
|
preprocessText("AB - Mail"),
|
||||||
|
equals("AB - Mail"),
|
||||||
|
reason:
|
||||||
|
"Should not remove '- Mail' because the first part is too short",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not match for texts such as check-in', () {
|
||||||
|
expect(
|
||||||
|
preprocessText("Check-in for flight"),
|
||||||
|
equals("Check-in for flight"),
|
||||||
|
reason: "Should not remove '-in'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Text processing edge cases', () {
|
||||||
|
test('empty string', () {
|
||||||
|
expect(
|
||||||
|
preprocessText(""),
|
||||||
|
equals(""),
|
||||||
|
reason: "Empty string returns empty string",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('exactly 20 chars', () {
|
||||||
|
const domain20Chars = "12345678901234567890"; // 20 characters
|
||||||
|
expect(
|
||||||
|
preprocessText("My Title - $domain20Chars"),
|
||||||
|
equals("My Title - $domain20Chars"),
|
||||||
|
reason:
|
||||||
|
"Should not remove suffix because it's exactly 20 chars long, not < 20",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiple delimiters, remove last only', () {
|
||||||
|
expect(
|
||||||
|
preprocessText("Complex - Title - SomethingSmall"),
|
||||||
|
equals("Complex Title"),
|
||||||
|
reason:
|
||||||
|
"Should remove only the last '- SomethingSmall', ignoring earlier delimiters",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('repeated delimiters', () {
|
||||||
|
expect(
|
||||||
|
preprocessText("Title --- Domain"),
|
||||||
|
equals("Title"),
|
||||||
|
reason: "Should remove the last chunk and filter out empty strings",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
preprocessText("Title || Domain"),
|
||||||
|
equals("Title"),
|
||||||
|
reason: "Should remove the last chunk with double pipe delimiters too",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('long trailing text', () {
|
||||||
|
const longDomain = "Useful information is present";
|
||||||
|
expect(
|
||||||
|
preprocessText("Some Title - $longDomain"),
|
||||||
|
equals("Some Title - $longDomain"),
|
||||||
|
reason: "Should not remove suffix if it's >= 20 characters",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+25
-4
@@ -46,9 +46,9 @@ const SMART_TAB_GROUPING_CONFIG = {
|
|||||||
*/
|
*/
|
||||||
function createModelInput(keywords, documents) {
|
function createModelInput(keywords, documents) {
|
||||||
if (!keywords || keywords.length === 0) {
|
if (!keywords || keywords.length === 0) {
|
||||||
return `Topic from keywords: titles: \n${documents.join(" \n")}`;
|
return `Topic from keywords: titles: \n${documents.slice(0, 3).join(" \n")}`;
|
||||||
}
|
}
|
||||||
return `Topic from keywords: ${keywords.join(", ")}. titles: \n${documents.join(" \n")}`;
|
return `Topic from keywords: ${keywords.join(", ")}. titles: \n${documents.slice(0, 3).join(" \n")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +81,6 @@ function cutAtDuplicateWords(phrase) {
|
|||||||
return phrase; // return original phrase
|
return phrase; // return original phrase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {MLEngine} engine the engine to check
|
* @param {MLEngine} engine the engine to check
|
||||||
@@ -96,7 +95,29 @@ this.ml = class extends ExtensionAPI {
|
|||||||
return {
|
return {
|
||||||
experiments: {
|
experiments: {
|
||||||
ml: {
|
ml: {
|
||||||
async containerTopic(keywords, documents) {
|
async generateEmbeddings(textToEmbedList) {
|
||||||
|
const inputData = {
|
||||||
|
inputArgs: textToEmbedList,
|
||||||
|
runOptions: {
|
||||||
|
pooling: "mean",
|
||||||
|
normalize: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEngineClosed(this.embeddingEngine)) {
|
||||||
|
this.embeddingEngine = await createEngine(SMART_TAB_GROUPING_CONFIG.embedding);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
args: [inputData.inputArgs],
|
||||||
|
options: inputData.runOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
const generated = await this.embeddingEngine.run(request);
|
||||||
|
|
||||||
|
return JSON.stringify(generated);
|
||||||
|
},
|
||||||
|
async predictTopic(keywords, documents) {
|
||||||
if (isEngineClosed(this.topicEngine)) {
|
if (isEngineClosed(this.topicEngine)) {
|
||||||
const {
|
const {
|
||||||
featureId,
|
featureId,
|
||||||
|
|||||||
+14
-4
@@ -26,15 +26,25 @@ function sendErrorForRequest(id) {
|
|||||||
port.onMessage.addListener(async (message) => {
|
port.onMessage.addListener(async (message) => {
|
||||||
let requestId = message["id"]
|
let requestId = message["id"]
|
||||||
switch (message["action"]) {
|
switch (message["action"]) {
|
||||||
case "getContainerTopic":
|
case "predictDocumentTopic": {
|
||||||
const documents = message["args"];
|
const documents = message["args"];
|
||||||
const keywords = (documents.length > 1)
|
const keywords = (documents.length > 1)
|
||||||
? await browser.experiments.nlp.extractKeywords([documents.slice(0, 3).join(" ")])
|
? await browser.experiments.nlp.extractKeywords([documents.slice(0, 3).join(" ")])
|
||||||
: [[]];
|
: [[]];
|
||||||
|
|
||||||
browser.experiments.ml.containerTopic(keywords[0], documents)
|
browser.experiments.ml.predictTopic(keywords[0], documents)
|
||||||
.then(sendJsonResultForRequest(requestId))
|
.then(sendJsonResultForRequest(requestId))
|
||||||
.catch(sendErrorForRequest(requestId))
|
.catch(sendErrorForRequest(requestId));
|
||||||
break
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "generateDocumentEmbeddings": {
|
||||||
|
const documents = message["args"];
|
||||||
|
await browser.experiments.ml.generateEmbeddings(documents)
|
||||||
|
.then(sendJsonResultForRequest(requestId))
|
||||||
|
.catch(sendErrorForRequest(requestId));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+17
-1
@@ -33,7 +33,23 @@
|
|||||||
"description": "Machine Learning utilities",
|
"description": "Machine Learning utilities",
|
||||||
"functions": [
|
"functions": [
|
||||||
{
|
{
|
||||||
"name": "containerTopic",
|
"name": "generateEmbeddings",
|
||||||
|
"type": "function",
|
||||||
|
"description": "Generate embeddings for a list of text strings using ML engine",
|
||||||
|
"async": true,
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "textToEmbedList",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Array of text strings to generate embeddings for"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "predictTopic",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"description": "Generate topic from keywords and documents using ML engine",
|
"description": "Generate topic from keywords and documents using ML engine",
|
||||||
"async": true,
|
"async": true,
|
||||||
|
|||||||
+36
-2
@@ -13,8 +13,8 @@ class GeckoMlApiImpl : GeckoMlApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getContainerTopic(titles: List<String>, callback: (Result<String>) -> Unit) {
|
override fun predictDocumentTopic(documents: List<String>, callback: (Result<String>) -> Unit) {
|
||||||
MLEngineFeature.scheduleRequest("getContainerTopic", titles.toJson(), object : ResultConsumer<JSONObject> {
|
MLEngineFeature.scheduleRequest("predictDocumentTopic", documents.toJson(), object : ResultConsumer<JSONObject> {
|
||||||
override fun success(result: JSONObject) {
|
override fun success(result: JSONObject) {
|
||||||
callback(Result.success(result.getString("result")))
|
callback(Result.success(result.getString("result")))
|
||||||
}
|
}
|
||||||
@@ -24,4 +24,38 @@ class GeckoMlApiImpl : GeckoMlApi {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun generateDocumentEmbeddings(
|
||||||
|
documents: List<String>,
|
||||||
|
callback: (Result<List<Any?>>) -> Unit
|
||||||
|
) {
|
||||||
|
MLEngineFeature.scheduleRequest("generateDocumentEmbeddings", documents.toJson(), object : ResultConsumer<JSONObject> {
|
||||||
|
override fun success(result: JSONObject) {
|
||||||
|
try {
|
||||||
|
val encodedResult = result.getString("result")
|
||||||
|
val decodedJsonArray = JSONArray(encodedResult)
|
||||||
|
val embeddings = mutableListOf<List<Double>>()
|
||||||
|
|
||||||
|
for (i in 0 until decodedJsonArray.length()) {
|
||||||
|
val embeddingArray = decodedJsonArray.getJSONArray(i)
|
||||||
|
val embedding = mutableListOf<Double>()
|
||||||
|
|
||||||
|
for (j in 0 until embeddingArray.length()) {
|
||||||
|
embedding.add(embeddingArray.getDouble(j))
|
||||||
|
}
|
||||||
|
embeddings.add(embedding)
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(Result.success(embeddings))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
callback(Result.failure(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||||
|
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+25
-4
@@ -3425,7 +3425,8 @@ interface GeckoPrefApi {
|
|||||||
}
|
}
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface GeckoMlApi {
|
interface GeckoMlApi {
|
||||||
fun getContainerTopic(titles: List<String>, callback: (Result<String>) -> Unit)
|
fun predictDocumentTopic(documents: List<String>, callback: (Result<String>) -> Unit)
|
||||||
|
fun generateDocumentEmbeddings(documents: List<String>, callback: (Result<List<Any?>>) -> Unit)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by GeckoMlApi. */
|
/** The codec used by GeckoMlApi. */
|
||||||
@@ -3437,12 +3438,32 @@ interface GeckoMlApi {
|
|||||||
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoMlApi?, messageChannelSuffix: String = "") {
|
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoMlApi?, messageChannelSuffix: String = "") {
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.getContainerTopic$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.predictDocumentTopic$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { message, reply ->
|
channel.setMessageHandler { message, reply ->
|
||||||
val args = message as List<Any?>
|
val args = message as List<Any?>
|
||||||
val titlesArg = args[0] as List<String>
|
val documentsArg = args[0] as List<String>
|
||||||
api.getContainerTopic(titlesArg) { result: Result<String> ->
|
api.predictDocumentTopic(documentsArg) { result: Result<String> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(GeckoPigeonUtils.wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(GeckoPigeonUtils.wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.generateDocumentEmbeddings$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val documentsArg = args[0] as List<String>
|
||||||
|
api.generateDocumentEmbeddings(documentsArg) { result: Result<List<Any?>> ->
|
||||||
val error = result.exceptionOrNull()
|
val error = result.exceptionOrNull()
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
reply.reply(GeckoPigeonUtils.wrapError(error))
|
reply.reply(GeckoPigeonUtils.wrapError(error))
|
||||||
|
|||||||
@@ -9,13 +9,23 @@ import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
|
|||||||
final _apiInstance = GeckoMlApi();
|
final _apiInstance = GeckoMlApi();
|
||||||
|
|
||||||
class GeckoMlService {
|
class GeckoMlService {
|
||||||
Future<String> getContainerTopic(Set<String> titles, {int maxCount = 8}) {
|
Future<String> predictDocumentTopic(Set<String> titles, {int maxCount = 10}) {
|
||||||
var selectedTitles = titles.toList();
|
var selectedTitles = titles.toList();
|
||||||
if (selectedTitles.length > maxCount) {
|
if (selectedTitles.length > maxCount) {
|
||||||
//TODO: Randomize for now, maybe use clusters later
|
//TODO: Randomize for now, maybe use clusters later
|
||||||
selectedTitles = (selectedTitles..shuffle()).take(maxCount).toList();
|
selectedTitles = (selectedTitles..shuffle()).take(maxCount).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _apiInstance.getContainerTopic(selectedTitles);
|
return _apiInstance.predictDocumentTopic(selectedTitles);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<List<double>>> generateDocumentEmbeddings(
|
||||||
|
List<String> documents,
|
||||||
|
) async {
|
||||||
|
final embeddings = await _apiInstance.generateDocumentEmbeddings(documents);
|
||||||
|
|
||||||
|
return embeddings
|
||||||
|
.map((values) => (values! as List).cast<double>())
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3968,14 +3968,14 @@ class GeckoMlApi {
|
|||||||
|
|
||||||
final String pigeonVar_messageChannelSuffix;
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
Future<String> getContainerTopic(List<String> titles) async {
|
Future<String> predictDocumentTopic(List<String> documents) async {
|
||||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.getContainerTopic$pigeonVar_messageChannelSuffix';
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.predictDocumentTopic$pigeonVar_messageChannelSuffix';
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
);
|
);
|
||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[titles]);
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[documents]);
|
||||||
final List<Object?>? pigeonVar_replyList =
|
final List<Object?>? pigeonVar_replyList =
|
||||||
await pigeonVar_sendFuture as List<Object?>?;
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
if (pigeonVar_replyList == null) {
|
if (pigeonVar_replyList == null) {
|
||||||
@@ -3995,6 +3995,34 @@ class GeckoMlApi {
|
|||||||
return (pigeonVar_replyList[0] as String?)!;
|
return (pigeonVar_replyList[0] as String?)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Object?>> generateDocumentEmbeddings(List<String> documents) async {
|
||||||
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.generateDocumentEmbeddings$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[documents]);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as List<Object?>?)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeckoBrowserExtensionApi {
|
class GeckoBrowserExtensionApi {
|
||||||
|
|||||||
@@ -956,7 +956,9 @@ abstract class GeckoPrefApi {
|
|||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class GeckoMlApi {
|
abstract class GeckoMlApi {
|
||||||
@async
|
@async
|
||||||
String getContainerTopic(List<String> titles);
|
String predictDocumentTopic(List<String> documents);
|
||||||
|
@async
|
||||||
|
List generateDocumentEmbeddings(List<String> documents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostApi()
|
@HostApi()
|
||||||
|
|||||||
Reference in New Issue
Block a user