intermediate
This commit is contained in:
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/core/logger.dart';
|
||||
import 'package:lensai/features/bangs/domain/providers.dart';
|
||||
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
@@ -22,7 +22,7 @@ GeckoSelectionActionService selectionActionService(
|
||||
service.setActions([
|
||||
SearchAction((text) async {
|
||||
final defaultSearchBang =
|
||||
await ref.read(kagiSearchBangDataProvider.future);
|
||||
await ref.read(defaultSearchBangDataProvider.future);
|
||||
|
||||
if (defaultSearchBang != null) {
|
||||
await ref
|
||||
@@ -34,7 +34,7 @@ GeckoSelectionActionService selectionActionService(
|
||||
}),
|
||||
PrivateSearchAction((text) async {
|
||||
final defaultSearchBang =
|
||||
await ref.read(kagiSearchBangDataProvider.future);
|
||||
await ref.read(defaultSearchBangDataProvider.future);
|
||||
|
||||
if (defaultSearchBang != null) {
|
||||
await ref.read(tabRepositoryProvider.notifier).addTab(
|
||||
@@ -96,6 +96,17 @@ GeckoAddonService addonService(Ref ref) {
|
||||
return service;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
GeckoTabContentService tabContentService(Ref ref) {
|
||||
final service = GeckoTabContentService.setUp();
|
||||
|
||||
ref.onDispose(() {
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class EngineReadyState extends _$EngineReadyState {
|
||||
@override
|
||||
@@ -133,5 +144,5 @@ class EngineReadyState extends _$EngineReadyState {
|
||||
|
||||
@Riverpod()
|
||||
Raw<TabSession> selectedTabSessionNotifier(Ref ref) {
|
||||
return ref.watch(tabSessionProvider(null).notifier);
|
||||
return ref.watch(tabSessionProvider(tabId: null).notifier);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'providers.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$selectionActionServiceHash() =>
|
||||
r'b5bc6b2bc5496ae84d61a74cc6ff052b55d2a3d3';
|
||||
r'd0c04262171976c21441297706273417e3d79b67';
|
||||
|
||||
/// See also [selectionActionService].
|
||||
@ProviderFor(selectionActionService)
|
||||
@@ -57,8 +57,25 @@ final addonServiceProvider = Provider<GeckoAddonService>.internal(
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef AddonServiceRef = ProviderRef<GeckoAddonService>;
|
||||
String _$tabContentServiceHash() => r'd9a991add907ecc138c62790883e59d8e9aa9266';
|
||||
|
||||
/// See also [tabContentService].
|
||||
@ProviderFor(tabContentService)
|
||||
final tabContentServiceProvider = Provider<GeckoTabContentService>.internal(
|
||||
tabContentService,
|
||||
name: r'tabContentServiceProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$tabContentServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef TabContentServiceRef = ProviderRef<GeckoTabContentService>;
|
||||
String _$selectedTabSessionNotifierHash() =>
|
||||
r'62fcd98a915566d55b5bcb297d87d675fe12786d';
|
||||
r'aef19991d5b05a2bb28e8a6ccb57dc75c2f3148f';
|
||||
|
||||
/// See also [selectedTabSessionNotifier].
|
||||
@ProviderFor(selectedTabSessionNotifier)
|
||||
|
||||
@@ -2,22 +2,16 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'selected_tab.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SelectedTab extends _$SelectedTab {
|
||||
late TabDatabase _db;
|
||||
|
||||
@override
|
||||
String? build() {
|
||||
final eventSerivce = ref.watch(eventServiceProvider);
|
||||
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
|
||||
ref.listen(
|
||||
fireImmediately: true,
|
||||
engineReadyStateProvider,
|
||||
@@ -31,10 +25,6 @@ class SelectedTab extends _$SelectedTab {
|
||||
final selectedTabSub = eventSerivce.selectedTabEvents.listen(
|
||||
(tabId) async {
|
||||
state = tabId;
|
||||
|
||||
if (tabId != null) {
|
||||
await _db.tabDao.touchTab(tabId, timestamp: DateTime.now());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'selected_tab.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$selectedTabHash() => r'e49fb1c3938a97f384673e158742e56479aa8a8f';
|
||||
String _$selectedTabHash() => r'f550ac5b471bab8f8e5d75ef4d7669cde769db9a';
|
||||
|
||||
/// See also [SelectedTab].
|
||||
@ProviderFor(SelectedTab)
|
||||
|
||||
@@ -3,22 +3,16 @@ import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab_list.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TabList extends _$TabList {
|
||||
late TabDatabase _db;
|
||||
|
||||
@override
|
||||
List<String> build() {
|
||||
final eventService = ref.watch(eventServiceProvider);
|
||||
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
|
||||
ref.listen(
|
||||
fireImmediately: true,
|
||||
engineReadyStateProvider,
|
||||
@@ -32,15 +26,7 @@ class TabList extends _$TabList {
|
||||
final tabListSub = eventService.tabListEvents.listen(
|
||||
(tabs) async {
|
||||
if (!const DeepCollectionEquality().equals(state, tabs)) {
|
||||
//Make sure we only syncing empty lists when there has been tabs
|
||||
//before as security measurement
|
||||
final syncTabs = tabs.isNotEmpty || state.isNotEmpty;
|
||||
|
||||
state = tabs;
|
||||
|
||||
if (syncTabs) {
|
||||
await _db.tabDao.syncTabs(retainTabIds: tabs);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'tab_list.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabListHash() => r'3c17372cc467736ce1cfba937999acf4de7345ef';
|
||||
String _$tabListHash() => r'039099bfa102a86fd73c0544ea71f93f91e9d352';
|
||||
|
||||
/// See also [TabList].
|
||||
@ProviderFor(TabList)
|
||||
|
||||
@@ -42,7 +42,7 @@ class TabSession extends _$TabSession {
|
||||
}
|
||||
|
||||
@override
|
||||
void build(String? tabId) {
|
||||
void build({required String? tabId}) {
|
||||
_sessionService = (tabId != null)
|
||||
? GeckoSessionService(tabId: tabId)
|
||||
: GeckoSessionService.forActiveTab();
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'tab_session.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabSessionHash() => r'42b44a4d47b9a551d002fdbc6337c14b2c327dde';
|
||||
String _$tabSessionHash() => r'e4fbbcd430037cc01adb799783fb7974a11a27bf';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -32,9 +32,9 @@ class _SystemHash {
|
||||
abstract class _$TabSession extends BuildlessAutoDisposeNotifier<void> {
|
||||
late final String? tabId;
|
||||
|
||||
void build(
|
||||
String? tabId,
|
||||
);
|
||||
void build({
|
||||
required String? tabId,
|
||||
});
|
||||
}
|
||||
|
||||
/// See also [TabSession].
|
||||
@@ -47,11 +47,11 @@ class TabSessionFamily extends Family<void> {
|
||||
const TabSessionFamily();
|
||||
|
||||
/// See also [TabSession].
|
||||
TabSessionProvider call(
|
||||
String? tabId,
|
||||
) {
|
||||
TabSessionProvider call({
|
||||
required String? tabId,
|
||||
}) {
|
||||
return TabSessionProvider(
|
||||
tabId,
|
||||
tabId: tabId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class TabSessionFamily extends Family<void> {
|
||||
covariant TabSessionProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.tabId,
|
||||
tabId: provider.tabId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,9 +83,9 @@ class TabSessionFamily extends Family<void> {
|
||||
class TabSessionProvider
|
||||
extends AutoDisposeNotifierProviderImpl<TabSession, void> {
|
||||
/// See also [TabSession].
|
||||
TabSessionProvider(
|
||||
String? tabId,
|
||||
) : this._internal(
|
||||
TabSessionProvider({
|
||||
required String? tabId,
|
||||
}) : this._internal(
|
||||
() => TabSession()..tabId = tabId,
|
||||
from: tabSessionProvider,
|
||||
name: r'tabSessionProvider',
|
||||
@@ -116,7 +116,7 @@ class TabSessionProvider
|
||||
covariant TabSession notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
tabId,
|
||||
tabId: tabId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,15 @@ import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/selected_tab.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/tabs/data/database/database.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||
import 'package:lensai/utils/debouncer.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
@@ -57,6 +62,10 @@ class TabRepository extends _$TabRepository {
|
||||
@override
|
||||
void build() {
|
||||
final eventSerivce = ref.watch(eventServiceProvider);
|
||||
final tabContentService = ref.watch(tabContentServiceProvider);
|
||||
|
||||
final tabStateDebouncer = Debouncer(const Duration(seconds: 3));
|
||||
Map<String, TabState>? debounceStartValue;
|
||||
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
|
||||
@@ -67,8 +76,58 @@ class TabRepository extends _$TabRepository {
|
||||
},
|
||||
);
|
||||
|
||||
final tabContentSub =
|
||||
tabContentService.tabContentStream.listen((content) async {
|
||||
await _db.tabDao.updateTab(
|
||||
content.tabId,
|
||||
extractedContentMarkdown: Value(content.extractedContentMarkdown),
|
||||
extractedContentPlain: Value(content.extractedContentPlain),
|
||||
fullContentMarkdown: Value(content.fullContentMarkdown),
|
||||
fullContentPlain: Value(content.fullContentPlain),
|
||||
);
|
||||
});
|
||||
|
||||
ref.listen(
|
||||
selectedTabProvider,
|
||||
(previous, tabId) async {
|
||||
if (tabId != null) {
|
||||
await _db.tabDao.touchTab(tabId, timestamp: DateTime.now());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ref.listen(
|
||||
tabListProvider,
|
||||
(previous, next) async {
|
||||
//Only sync tabs if there has been a previous value or is not empty
|
||||
final syncTabs = next.isNotEmpty || (previous?.isNotEmpty ?? false);
|
||||
|
||||
if (syncTabs) {
|
||||
await _db.tabDao.syncTabs(retainTabIds: next);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ref.listen(
|
||||
tabStatesProvider,
|
||||
(previous, next) async {
|
||||
//Since state changes occure pretty often and our map always contains
|
||||
//the latest state, we cache the value before starting debouncing and
|
||||
//later diff to that, to avoid frequent database writes
|
||||
if (!tabStateDebouncer.isDebouncing) {
|
||||
debounceStartValue = previous;
|
||||
}
|
||||
|
||||
tabStateDebouncer.eventOccured(() async {
|
||||
await _db.tabDao.updateTabs(debounceStartValue, next);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ref.onDispose(() {
|
||||
tabStateDebouncer.dispose();
|
||||
unawaited(tabAddedSub.cancel());
|
||||
unawaited(tabContentSub.cancel());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'tab.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabRepositoryHash() => r'05040223b83cd5ff80afaf89407043a10e4777cb';
|
||||
String _$tabRepositoryHash() => r'b151be1c18d87eff3a15f4b70f4e82fcc11047ec';
|
||||
|
||||
/// See also [TabRepository].
|
||||
@ProviderFor(TabRepository)
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/data/models/web_page_info.dart';
|
||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||
import 'package:lensai/features/bangs/domain/providers.dart';
|
||||
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:lensai/features/bangs/presentation/widgets/site_search.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
||||
@@ -92,7 +92,7 @@ class WebPageDialog extends HookConsumerWidget {
|
||||
onPressed: () async {
|
||||
if (formKey.currentState?.validate() ?? false) {
|
||||
await ref
|
||||
.read(tabSessionProvider(null).notifier)
|
||||
.read(tabSessionProvider(tabId: null).notifier)
|
||||
.loadUrl(
|
||||
url: uri_parser.tryParseUrl(
|
||||
urlTextController.text,
|
||||
|
||||
+31
-49
@@ -19,6 +19,7 @@ import 'package:lensai/features/geckoview/features/browser/domain/services/engin
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||
@@ -47,6 +48,9 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
//Init here to avoid dispose errors of menu items when called
|
||||
final addonService = ref.watch(addonServiceProvider);
|
||||
|
||||
final displayedSheet = ref.watch(bottomSheetControllerProvider);
|
||||
final displayedOverlayDialog = ref.watch(overlayDialogControllerProvider);
|
||||
|
||||
@@ -186,14 +190,15 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
color: (activeTool == KagiTool.search)
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(createTabStreamProvider.notifier)
|
||||
.createTab(
|
||||
CreateTabSheet(
|
||||
preferredTool: KagiTool.search,
|
||||
),
|
||||
);
|
||||
onPressed: () async {
|
||||
// ref
|
||||
// .read(createTabStreamProvider.notifier)
|
||||
// .createTab(
|
||||
// CreateTabSheet(
|
||||
// preferredTool: KagiTool.search,
|
||||
// ),
|
||||
// );
|
||||
await context.push(SearchRoute().location);
|
||||
},
|
||||
icon: Icon(KagiTool.search.icon),
|
||||
),
|
||||
@@ -337,30 +342,12 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
(extension) => IconButton(
|
||||
onPressed: () async {
|
||||
//Use parents .ref because after onPressed this consumer gets disposed already
|
||||
await ref
|
||||
.read(addonServiceProvider)
|
||||
.invokeAddonAction(
|
||||
extension.extensionId,
|
||||
WebExtensionActionType.page,
|
||||
);
|
||||
await addonService.invokeAddonAction(
|
||||
extension.extensionId,
|
||||
WebExtensionActionType.page,
|
||||
);
|
||||
},
|
||||
icon: Badge(
|
||||
isLabelVisible:
|
||||
extension.badgeText?.isNotEmpty ??
|
||||
false,
|
||||
label: (extension.badgeText?.isNotEmpty ==
|
||||
true)
|
||||
? Text(extension.badgeText!)
|
||||
: null,
|
||||
textColor: extension.badgeTextColor,
|
||||
backgroundColor:
|
||||
extension.badgeBackgroundColor,
|
||||
child: RawImage(
|
||||
image: extension.icon?.value,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
icon: ExtensionBadgeIcon(extension),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -383,8 +370,8 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
child: const Text('Settings'),
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, childRef, child) {
|
||||
final browserExtensions = childRef.watch(
|
||||
builder: (context, ref, child) {
|
||||
final browserExtensions = ref.watch(
|
||||
webExtensionsStateProvider(
|
||||
WebExtensionActionType.browser,
|
||||
).select((value) => value.values.toList()),
|
||||
@@ -396,24 +383,18 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
(extension) => MenuItemButton(
|
||||
onPressed: () async {
|
||||
//Use parents .ref because after onPressed this consumer gets disposed already
|
||||
await ref
|
||||
.read(addonServiceProvider)
|
||||
.invokeAddonAction(
|
||||
extension.extensionId,
|
||||
WebExtensionActionType.browser,
|
||||
);
|
||||
await addonService.invokeAddonAction(
|
||||
extension.extensionId,
|
||||
WebExtensionActionType.browser,
|
||||
);
|
||||
},
|
||||
leadingIcon: RawImage(
|
||||
image: extension.icon?.value,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
leadingIcon: ExtensionBadgeIcon(extension),
|
||||
child: Text(extension.title ?? ''),
|
||||
),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
await childRef
|
||||
await ref
|
||||
.read(addonServiceProvider)
|
||||
.startAddonManagerActivity();
|
||||
},
|
||||
@@ -530,7 +511,8 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(selectedTabId).notifier,
|
||||
tabSessionProvider(tabId: selectedTabId)
|
||||
.notifier,
|
||||
);
|
||||
|
||||
await controller.reload();
|
||||
@@ -556,7 +538,7 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
onPressed: () async {
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(
|
||||
selectedTabId,
|
||||
tabId: selectedTabId,
|
||||
).notifier,
|
||||
);
|
||||
|
||||
@@ -588,7 +570,7 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
? () async {
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(
|
||||
selectedTabId,
|
||||
tabId: selectedTabId,
|
||||
).notifier,
|
||||
);
|
||||
|
||||
@@ -664,7 +646,7 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
lastBackButtonPress.value = null;
|
||||
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(selectedTabId).notifier,
|
||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||
);
|
||||
|
||||
await controller.goBack();
|
||||
@@ -7,6 +7,7 @@ import 'package:lensai/core/logger.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||
|
||||
class BrowserView extends StatefulHookConsumerWidget {
|
||||
final Duration screenshotPeriod;
|
||||
@@ -84,6 +85,8 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
//Initialize and register dependencies
|
||||
ref.listenManual(tabRepositoryProvider, (previous, next) {});
|
||||
|
||||
ref.listenManual(
|
||||
selectionActionServiceProvider,
|
||||
(previous, next) {},
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/web_extension_state.dart';
|
||||
|
||||
class ExtensionBadgeIcon extends StatelessWidget {
|
||||
final WebExtensionState state;
|
||||
|
||||
const ExtensionBadgeIcon(this.state);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasBadge = state.badgeText?.isNotEmpty ?? false;
|
||||
|
||||
return Badge(
|
||||
isLabelVisible: hasBadge,
|
||||
label: hasBadge ? Text(state.badgeText!) : null,
|
||||
textColor: state.badgeTextColor,
|
||||
backgroundColor: state.badgeBackgroundColor,
|
||||
child: RawImage(
|
||||
image: state.icon?.value,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+25
-1
@@ -67,7 +67,31 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
||||
),
|
||||
],
|
||||
),
|
||||
ContainerChips(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final selectedContainer = ref.watch(
|
||||
selectedContainerDataProvider
|
||||
.select((value) => value.valueOrNull),
|
||||
);
|
||||
|
||||
return ContainerChips(
|
||||
selectedContainer: selectedContainer,
|
||||
onSelected: (container) {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.setContainerId(container.id);
|
||||
},
|
||||
onDeleted: (container) async {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.clearContainer();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:lensai/core/logger.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
||||
import 'package:lexo_rank/lexo_rank.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
@@ -10,6 +14,14 @@ part 'tab.g.dart';
|
||||
class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
||||
TabDao(super.db);
|
||||
|
||||
UpdateStatement<Tab, TabData> _updateByIdStatement(String id) =>
|
||||
db.tab.update()..where((t) => t.id.equals(id));
|
||||
|
||||
Selectable<TabData> allTabData() => db.tab.select();
|
||||
|
||||
SingleOrNullSelectable<TabData> tabDataById(String id) =>
|
||||
db.tab.select()..where((t) => t.id.equals(id));
|
||||
|
||||
Selectable<String> containerTabIds(String? containerId) {
|
||||
final query = selectOnly(db.tab)
|
||||
..addColumns([db.tab.id])
|
||||
@@ -74,7 +86,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
||||
String id, {
|
||||
required String? containerId,
|
||||
}) {
|
||||
final statement = db.tab.update()..where((t) => t.id.equals(id));
|
||||
final statement = _updateByIdStatement(id);
|
||||
return statement.write(
|
||||
TabCompanion(containerId: Value(containerId)),
|
||||
);
|
||||
@@ -84,7 +96,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
||||
String id, {
|
||||
required String orderKey,
|
||||
}) {
|
||||
final statement = db.tab.update()..where((t) => t.id.equals(id));
|
||||
final statement = _updateByIdStatement(id);
|
||||
return statement.write(
|
||||
TabCompanion(orderKey: Value(orderKey)),
|
||||
);
|
||||
@@ -94,12 +106,71 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
||||
String id, {
|
||||
required DateTime timestamp,
|
||||
}) {
|
||||
final statement = db.tab.update()..where((t) => t.id.equals(id));
|
||||
final statement = _updateByIdStatement(id);
|
||||
return statement.write(
|
||||
TabCompanion(timestamp: Value(timestamp)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateTab(
|
||||
String id, {
|
||||
Value<String?> url = const Value.absent(),
|
||||
Value<String?> title = const Value.absent(),
|
||||
Value<String?> extractedContentMarkdown = const Value.absent(),
|
||||
Value<String?> extractedContentPlain = const Value.absent(),
|
||||
Value<String?> fullContentMarkdown = const Value.absent(),
|
||||
Value<String?> fullContentPlain = const Value.absent(),
|
||||
}) async {
|
||||
final doUpdate = url != const Value.absent() ||
|
||||
title != const Value.absent() ||
|
||||
extractedContentMarkdown != const Value.absent() ||
|
||||
extractedContentPlain != const Value.absent() ||
|
||||
fullContentMarkdown != const Value.absent() ||
|
||||
fullContentPlain != const Value.absent();
|
||||
|
||||
if (doUpdate) {
|
||||
final statement = _updateByIdStatement(id);
|
||||
await statement.write(
|
||||
TabCompanion(
|
||||
url: url,
|
||||
title: title,
|
||||
extractedContentMarkdown: extractedContentMarkdown,
|
||||
extractedContentPlain: extractedContentPlain,
|
||||
fullContentMarkdown: fullContentMarkdown,
|
||||
fullContentPlain: fullContentPlain,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTabs(
|
||||
Map<String, TabState>? previous,
|
||||
Map<String, TabState> next,
|
||||
) async {
|
||||
await batch((batch) async {
|
||||
for (final state in next.values) {
|
||||
final previousState = previous?[state.id];
|
||||
|
||||
if (previousState == null ||
|
||||
previousState.url != state.url ||
|
||||
previousState.title != state.title) {
|
||||
batch.update(
|
||||
db.tab,
|
||||
TabCompanion(
|
||||
url: (previousState?.url != state.url)
|
||||
? Value(state.url.toString())
|
||||
: const Value.absent(),
|
||||
title: (previousState?.title != state.title)
|
||||
? Value(state.title)
|
||||
: const Value.absent(),
|
||||
),
|
||||
where: (t) => t.id.equals(state.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncTabs({required List<String> retainTabIds}) async {
|
||||
return db.transaction(() async {
|
||||
await (db.tab.delete()..where((t) => t.id.isNotIn(retainTabIds))).go();
|
||||
@@ -125,4 +196,30 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Selectable<TabQueryResult> queryTabs({
|
||||
required String matchPrefix,
|
||||
required String matchSuffix,
|
||||
required String ellipsis,
|
||||
required int snippetLength,
|
||||
required String searchString,
|
||||
}) {
|
||||
final ftsQuery = db.buildFtsQuery(searchString);
|
||||
|
||||
if (ftsQuery.isNotEmpty) {
|
||||
return db.queryTabsFullContent(
|
||||
query: db.buildFtsQuery(searchString),
|
||||
snippetLength: snippetLength,
|
||||
beforeMatch: matchPrefix,
|
||||
afterMatch: matchSuffix,
|
||||
ellipsis: ellipsis,
|
||||
);
|
||||
} else {
|
||||
return db.queryTabsBasic(
|
||||
query: db.buildLikeQuery(searchString),
|
||||
beforeMatch: matchPrefix,
|
||||
afterMatch: matchSuffix,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import 'package:lensai/data/database/converters/icon_data.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/container.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
||||
import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart';
|
||||
import 'package:lensai/features/search/domain/fts_tokenizer.dart';
|
||||
import 'package:lensai/features/search/domain/unix_tokenizer.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@@ -13,10 +17,40 @@ part 'database.g.dart';
|
||||
include: {'database.drift'},
|
||||
daos: [ContainerDao, TabDao],
|
||||
)
|
||||
class TabDatabase extends _$TabDatabase {
|
||||
class TabDatabase extends _$TabDatabase implements IQueryBuilder {
|
||||
@override
|
||||
final int schemaVersion = 2;
|
||||
|
||||
@override
|
||||
final int ftsTokenLimit = 10;
|
||||
@override
|
||||
final int ftsMinTokenLength = 3;
|
||||
|
||||
@override
|
||||
String buildFtsQuery(String input) {
|
||||
final ftsQueryBuilder = TrigramQueryBuilder.tokenize(
|
||||
input: input,
|
||||
minTokenLength: ftsMinTokenLength,
|
||||
tokenLimit: ftsTokenLimit,
|
||||
);
|
||||
|
||||
if (ftsQueryBuilder.hasTokens) {
|
||||
return ftsQueryBuilder.build();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
String buildLikeQuery(String input) {
|
||||
final likeQueryBuilder = UnixLikeQueryBuilder.tokenize(
|
||||
input: input,
|
||||
minTokenLength: 1,
|
||||
tokenLimit: 5,
|
||||
);
|
||||
|
||||
return likeQueryBuilder.build();
|
||||
}
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onUpgrade: (m, from, to) async {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:lensai/data/database/converters/color.dart';
|
||||
import 'package:lensai/data/database/converters/icon_data.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
||||
|
||||
CREATE TABLE container (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
@@ -14,9 +15,45 @@ CREATE TABLE tab (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
container_id TEXT REFERENCES container (id) ON DELETE CASCADE,
|
||||
order_key TEXT NOT NULL,
|
||||
url TEXT,
|
||||
title TEXT,
|
||||
extracted_content_markdown TEXT,
|
||||
extracted_content_plain TEXT,
|
||||
full_content_markdown TEXT,
|
||||
full_content_plain TEXT,
|
||||
timestamp DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE tab_fts
|
||||
USING fts5(
|
||||
title,
|
||||
url,
|
||||
extracted_content_plain,
|
||||
full_content_plain,
|
||||
content=tab,
|
||||
tokenize="trigram"
|
||||
);
|
||||
|
||||
-- Triggers to keep the FTS index up to date.
|
||||
CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN
|
||||
INSERT INTO
|
||||
tab_fts(rowid, title, url, extracted_content_plain, full_content_plain)
|
||||
VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);
|
||||
END;
|
||||
CREATE TRIGGER tab_after_delete AFTER DELETE ON tab BEGIN
|
||||
INSERT INTO
|
||||
tab_fts(tab_fts, rowid, title, url, extracted_content_plain, full_content_plain)
|
||||
VALUES('delete', old.rowid, old.title, old.url, old.extracted_content_plain, old.full_content_plain);
|
||||
END;
|
||||
CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN
|
||||
INSERT INTO
|
||||
tab_fts(tab_fts, rowid, title, url, extracted_content_plain, full_content_plain)
|
||||
VALUES('delete', old.rowid, old.title, old.url, old.extracted_content_plain, old.full_content_plain);
|
||||
INSERT INTO
|
||||
tab_fts(rowid, title, url, extracted_content_plain, full_content_plain)
|
||||
VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);
|
||||
END;
|
||||
|
||||
containersWithCount WITH ContainerDataWithCount:
|
||||
SELECT
|
||||
container.*,
|
||||
@@ -66,4 +103,53 @@ orderKeyAfterTab(:tab_id AS TEXT, :container_id AS TEXT OR NULL):
|
||||
)
|
||||
SELECT lexo_rank_reorder_after(order_key, next_order_key)
|
||||
FROM ordered_table
|
||||
WHERE id = :tab_id;
|
||||
WHERE id = :tab_id;
|
||||
|
||||
queryTabsBasic WITH TabQueryResult:
|
||||
WITH weights AS (
|
||||
SELECT
|
||||
-- Customize these weights (higher = more important)
|
||||
10.0 as title_weight, -- Title matches are most important
|
||||
5.0 as url_weight -- URL matches are quite important
|
||||
)
|
||||
SELECT
|
||||
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
|
||||
FROM tab_fts fts
|
||||
INNER JOIN
|
||||
tab t ON t.rowid = fts.rowid
|
||||
CROSS JOIN weights
|
||||
WHERE
|
||||
fts.title LIKE :query OR
|
||||
fts.url LIKE :query
|
||||
ORDER BY
|
||||
weighted_rank ASC,
|
||||
t.timestamp DESC;
|
||||
|
||||
queryTabsFullContent WITH TabQueryResult:
|
||||
WITH weights AS (
|
||||
SELECT
|
||||
-- Customize these weights (higher = more important)
|
||||
10.0 as title_weight, -- Title matches are most important
|
||||
5.0 as url_weight, -- URL matches are quite important
|
||||
3.0 as extracted_weight, -- Extracted content matches
|
||||
1.0 as full_weight -- Full content matches less important
|
||||
)
|
||||
SELECT
|
||||
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
|
||||
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
|
||||
snippet(tab_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS extracted_content,
|
||||
snippet(tab_fts, 3, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS full_content,
|
||||
(
|
||||
bm25(tab_fts, weights.title_weight, weights.url_weight,
|
||||
weights.extracted_weight, weights.full_weight)
|
||||
) AS weighted_rank
|
||||
FROM tab_fts(:query) fts
|
||||
INNER JOIN
|
||||
tab t ON t.rowid = fts.rowid
|
||||
CROSS JOIN weights
|
||||
ORDER BY
|
||||
weighted_rank ASC,
|
||||
t.timestamp DESC;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
class TabQueryResult {
|
||||
final String title;
|
||||
final String url;
|
||||
|
||||
final String? extractedContent;
|
||||
final String? fullContent;
|
||||
|
||||
final double weightedRank;
|
||||
|
||||
TabQueryResult({
|
||||
required this.title,
|
||||
required this.url,
|
||||
this.extractedContent,
|
||||
this.fullContent,
|
||||
required this.weightedRank,
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:lensai/features/geckoview/features/tabs/data/models/container_da
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/utils/color_palette.dart';
|
||||
import 'package:lensai/features/search/util/tokenized_filter.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
@@ -35,6 +36,28 @@ Stream<List<ContainerDataWithCount>> containersWithCount(
|
||||
return db.containersWithCount().watch();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
AsyncValue<List<ContainerDataWithCount>> filteredContainersWithCount(
|
||||
Ref ref,
|
||||
String? searchText,
|
||||
) {
|
||||
return ref.watch(
|
||||
containersWithCountProvider.select((value) {
|
||||
if (searchText?.isEmpty ?? true) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.whenData(
|
||||
(cb) => TokenizedFilter(
|
||||
items: cb,
|
||||
toString: (item) => item.name,
|
||||
query: searchText!,
|
||||
).filtered,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<String?> tabContainerId(Ref ref, String tabId) {
|
||||
final db = ref.watch(tabDatabaseProvider);
|
||||
|
||||
@@ -45,7 +45,8 @@ final containersWithCountProvider =
|
||||
// ignore: unused_element
|
||||
typedef ContainersWithCountRef
|
||||
= AutoDisposeStreamProviderRef<List<ContainerDataWithCount>>;
|
||||
String _$tabContainerIdHash() => r'726ddd000d23935a2c8ae39c7957412fab16016d';
|
||||
String _$filteredContainersWithCountHash() =>
|
||||
r'3a7284df29182a32f9b7831867a3a0b3e171b382';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -68,6 +69,145 @@ class _SystemHash {
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [filteredContainersWithCount].
|
||||
@ProviderFor(filteredContainersWithCount)
|
||||
const filteredContainersWithCountProvider = FilteredContainersWithCountFamily();
|
||||
|
||||
/// See also [filteredContainersWithCount].
|
||||
class FilteredContainersWithCountFamily
|
||||
extends Family<AsyncValue<List<ContainerDataWithCount>>> {
|
||||
/// See also [filteredContainersWithCount].
|
||||
const FilteredContainersWithCountFamily();
|
||||
|
||||
/// See also [filteredContainersWithCount].
|
||||
FilteredContainersWithCountProvider call(
|
||||
String? searchText,
|
||||
) {
|
||||
return FilteredContainersWithCountProvider(
|
||||
searchText,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FilteredContainersWithCountProvider getProviderOverride(
|
||||
covariant FilteredContainersWithCountProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.searchText,
|
||||
);
|
||||
}
|
||||
|
||||
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'filteredContainersWithCountProvider';
|
||||
}
|
||||
|
||||
/// See also [filteredContainersWithCount].
|
||||
class FilteredContainersWithCountProvider
|
||||
extends AutoDisposeProvider<AsyncValue<List<ContainerDataWithCount>>> {
|
||||
/// See also [filteredContainersWithCount].
|
||||
FilteredContainersWithCountProvider(
|
||||
String? searchText,
|
||||
) : this._internal(
|
||||
(ref) => filteredContainersWithCount(
|
||||
ref as FilteredContainersWithCountRef,
|
||||
searchText,
|
||||
),
|
||||
from: filteredContainersWithCountProvider,
|
||||
name: r'filteredContainersWithCountProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$filteredContainersWithCountHash,
|
||||
dependencies: FilteredContainersWithCountFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
FilteredContainersWithCountFamily._allTransitiveDependencies,
|
||||
searchText: searchText,
|
||||
);
|
||||
|
||||
FilteredContainersWithCountProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.searchText,
|
||||
}) : super.internal();
|
||||
|
||||
final String? searchText;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
AsyncValue<List<ContainerDataWithCount>> Function(
|
||||
FilteredContainersWithCountRef provider)
|
||||
create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: FilteredContainersWithCountProvider._internal(
|
||||
(ref) => create(ref as FilteredContainersWithCountRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
searchText: searchText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeProviderElement<AsyncValue<List<ContainerDataWithCount>>>
|
||||
createElement() {
|
||||
return _FilteredContainersWithCountProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FilteredContainersWithCountProvider &&
|
||||
other.searchText == searchText;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, searchText.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin FilteredContainersWithCountRef
|
||||
on AutoDisposeProviderRef<AsyncValue<List<ContainerDataWithCount>>> {
|
||||
/// The parameter `searchText` of this provider.
|
||||
String? get searchText;
|
||||
}
|
||||
|
||||
class _FilteredContainersWithCountProviderElement
|
||||
extends AutoDisposeProviderElement<AsyncValue<List<ContainerDataWithCount>>>
|
||||
with FilteredContainersWithCountRef {
|
||||
_FilteredContainersWithCountProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String? get searchText =>
|
||||
(origin as FilteredContainersWithCountProvider).searchText;
|
||||
}
|
||||
|
||||
String _$tabContainerIdHash() => r'726ddd000d23935a2c8ae39c7957412fab16016d';
|
||||
|
||||
/// See also [tabContainerId].
|
||||
@ProviderFor(tabContainerId)
|
||||
const tabContainerIdProvider = TabContainerIdFamily();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab_search.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
class TabSearchRepository extends _$TabSearchRepository {
|
||||
late TabDatabase _db;
|
||||
|
||||
Future<void> addQuery(
|
||||
String input, {
|
||||
int snippetLength = 120,
|
||||
String matchPrefix = '***',
|
||||
String matchSuffix = '***',
|
||||
String ellipsis = '…',
|
||||
}) async {
|
||||
if (input.isNotEmpty) {
|
||||
state = await AsyncValue.guard(
|
||||
() => _db.tabDao
|
||||
.queryTabs(
|
||||
matchPrefix: matchPrefix,
|
||||
matchSuffix: matchSuffix,
|
||||
ellipsis: ellipsis,
|
||||
snippetLength: snippetLength,
|
||||
searchString: input,
|
||||
)
|
||||
.get(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TabQueryResult>> build() {
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
|
||||
return Future.value([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab_search.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabSearchRepositoryHash() =>
|
||||
r'211ca4d7e9a744b5f8a32970ba3a6df721fc05e1';
|
||||
|
||||
/// 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,
|
||||
);
|
||||
|
||||
typedef _$TabSearchRepository = AutoDisposeAsyncNotifier<List<TabQueryResult>>;
|
||||
// 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
|
||||
+42
-27
@@ -1,25 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
||||
|
||||
class ContainerChips extends HookConsumerWidget {
|
||||
final bool displayMenu;
|
||||
|
||||
final ContainerData? selectedContainer;
|
||||
final void Function(ContainerDataWithCount)? onSelected;
|
||||
final void Function(ContainerDataWithCount)? onDeleted;
|
||||
|
||||
final TextEditingController? searchTextController;
|
||||
|
||||
const ContainerChips({
|
||||
required this.selectedContainer,
|
||||
required this.onSelected,
|
||||
required this.onDeleted,
|
||||
this.searchTextController,
|
||||
this.displayMenu = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final containersAsync = ref.watch(containersWithCountProvider);
|
||||
final selectedContainer = ref.watch(
|
||||
selectedContainerDataProvider.select((value) => value.valueOrNull),
|
||||
final searchText = useListenableSelector(
|
||||
searchTextController,
|
||||
() => searchTextController?.text,
|
||||
);
|
||||
|
||||
final containersAsync =
|
||||
ref.watch(filteredContainersWithCountProvider(searchText));
|
||||
|
||||
return containersAsync.when(
|
||||
data: (availableContainers) => SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
if (selectedContainer != null || availableContainers.isNotEmpty)
|
||||
Expanded(
|
||||
child: SelectableChips(
|
||||
@@ -38,34 +57,30 @@ class ContainerChips extends HookConsumerWidget {
|
||||
itemBadgeCount: (container) => container.tabCount,
|
||||
availableItems: availableContainers,
|
||||
selectedItem: selectedContainer,
|
||||
onSelected: (container) {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.setContainerId(container.id);
|
||||
},
|
||||
onDeleted: (container) async {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.clearContainer();
|
||||
},
|
||||
onSelected: onSelected,
|
||||
onDeleted: onDeleted,
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Press '>' to manage Containers.",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
Visibility(
|
||||
visible: displayMenu,
|
||||
child: Expanded(
|
||||
child: Text(
|
||||
"Press '>' to manage Containers.",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context.push(ContainerListRoute().location);
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
if (displayMenu)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context.push(ContainerListRoute().location);
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user