intermediate

This commit is contained in:
Fabian Freund
2024-12-12 09:44:33 +01:00
parent 0710116feb
commit 6b2ae28828
108 changed files with 5531 additions and 2265 deletions
@@ -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,
@@ -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) {},
@@ -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,
),
);
}
}
@@ -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
@@ -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),
),
],
),
),