background fetch running

This commit is contained in:
Fabian Freund
2025-03-05 14:27:13 +01:00
parent 20bdc4df3e
commit 70d1c1b50e
62 changed files with 898 additions and 173 deletions
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -25,8 +23,8 @@ class SelectedTab extends _$SelectedTab {
state = tabId;
});
ref.onDispose(() {
unawaited(selectedTabSub.cancel());
ref.onDispose(() async {
await selectedTabSub.cancel();
});
return null;
@@ -6,7 +6,7 @@ part of 'selected_tab.dart';
// RiverpodGenerator
// **************************************************************************
String _$selectedTabHash() => r'250f54fe9a56d14876e0979e5370eb76879da11c';
String _$selectedTabHash() => r'12b670dd76c3416e8d62bafd63bf69a83a603e3a';
/// See also [SelectedTab].
@ProviderFor(SelectedTab)
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
@@ -30,8 +28,8 @@ class TabList extends _$TabList {
}
});
ref.onDispose(() {
unawaited(tabListSub.cancel());
ref.onDispose(() async {
await tabListSub.cancel();
});
return EquatableValue([]);
@@ -6,7 +6,7 @@ part of 'tab_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$tabListHash() => r'2b98ccdf62c15670e0a12e38064e8186e6555cf8';
String _$tabListHash() => r'9507bcc75dc69a55aba14334cdef0128a5fcf252';
/// See also [TabList].
@ProviderFor(TabList)
@@ -165,9 +165,9 @@ class TabStates extends _$TabStates {
}
});
ref.onDispose(() {
ref.onDispose(() async {
for (final sub in subscriptions) {
unawaited(sub.cancel());
await sub.cancel();
}
});
@@ -160,7 +160,7 @@ final selectedTabStateProvider = AutoDisposeProvider<TabState?>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef SelectedTabStateRef = AutoDisposeProviderRef<TabState?>;
String _$tabStatesHash() => r'b136dbf6d68b67f0fe0db302a4147b67789db853';
String _$tabStatesHash() => r'3a906aec8c4e650de8d5d2461ff45a99b8a0a3b9';
/// See also [TabStates].
@ProviderFor(TabStates)
@@ -86,9 +86,9 @@ class WebExtensionsState extends _$WebExtensionsState {
],
};
ref.onDispose(() {
ref.onDispose(() async {
for (final sub in subscriptions) {
unawaited(sub.cancel());
await sub.cancel();
}
});
@@ -7,7 +7,7 @@ part of 'web_extensions_state.dart';
// **************************************************************************
String _$webExtensionsStateHash() =>
r'eab924e9c17e805a3c29065289be5d1c8ffe3bc9';
r'6fb44ab543250f2b2554e410ee749fdd035cd4e7';
/// Copied from Dart SDK
class _SystemHash {
@@ -167,10 +167,10 @@ class TabRepository extends _$TabRepository {
});
});
ref.onDispose(() {
ref.onDispose(() async {
tabStateDebouncer.dispose();
unawaited(tabAddedSub.cancel());
unawaited(tabContentSub.cancel());
await tabAddedSub.cancel();
await tabContentSub.cancel();
});
}
}
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator
// **************************************************************************
String _$tabRepositoryHash() => r'69e283b820d86388f4d80e71e08761e2b6890507';
String _$tabRepositoryHash() => r'35c078a841d815aa2452c07b52c4cf0cbcec7e68';
/// See also [TabRepository].
@ProviderFor(TabRepository)
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart';
@@ -48,8 +46,8 @@ class SelectedBangData extends _$SelectedBangData {
state = value;
});
ref.onDispose(() {
unawaited(subscription.cancel());
ref.onDispose(() async {
await subscription.cancel();
});
return null;
@@ -719,7 +719,7 @@ class _SelectedBangTriggerProviderElement
String? get domain => (origin as SelectedBangTriggerProvider).domain;
}
String _$selectedBangDataHash() => r'28c394e5989e108870ca5733e916b80b3cf8972c';
String _$selectedBangDataHash() => r'666858556efe59d2de38a04fda30e8ec227c6675';
abstract class _$SelectedBangData
extends BuildlessAutoDisposeNotifier<BangData?> {
@@ -4,10 +4,10 @@ import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/tor/domain/services/tor_proxy.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'proxy_settings_replucation.g.dart';
part 'proxy_settings_replication.g.dart';
@Riverpod(keepAlive: true)
class ProxySettingsReplucation extends _$ProxySettingsReplucation {
class ProxySettingsReplication extends _$ProxySettingsReplication {
final _service = GeckoContainerProxyService();
@override
@@ -1,28 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'proxy_settings_replucation.dart';
part of 'proxy_settings_replication.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$proxySettingsReplucationHash() =>
r'b00fc9a9c8b385a0f8a5830e9ba1c329d340c0ae';
String _$proxySettingsReplicationHash() =>
r'1982923eeb0bcd6d606683fde70835ec7aff601b';
/// See also [ProxySettingsReplucation].
@ProviderFor(ProxySettingsReplucation)
final proxySettingsReplucationProvider =
NotifierProvider<ProxySettingsReplucation, void>.internal(
ProxySettingsReplucation.new,
name: r'proxySettingsReplucationProvider',
/// See also [ProxySettingsReplication].
@ProviderFor(ProxySettingsReplication)
final proxySettingsReplicationProvider =
NotifierProvider<ProxySettingsReplication, void>.internal(
ProxySettingsReplication.new,
name: r'proxySettingsReplicationProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$proxySettingsReplucationHash,
: _$proxySettingsReplicationHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ProxySettingsReplucation = Notifier<void>;
typedef _$ProxySettingsReplication = Notifier<void>;
// 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
@@ -15,11 +15,12 @@ import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/browser_data.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/engine_settings_replication.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/proxy_settings_replucation.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart';
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart';
import 'package:lensai/features/user/domain/repositories/cache.dart';
import 'package:lensai/features/user/domain/repositories/general_settings.dart';
import 'package:lensai/features/user/domain/services/local_authentication.dart';
import 'package:lensai/features/web_feed/domain/services/article_content_processor.dart';
class BrowserView extends StatefulHookConsumerWidget {
final Duration screenshotPeriod;
@@ -151,7 +152,12 @@ class _BrowserViewState extends ConsumerState<BrowserView>
(previous, next) {},
);
ref.listenManual(proxySettingsReplucationProvider, (previous, next) {});
ref.listenManual(proxySettingsReplicationProvider, (previous, next) {});
ref.listenManual(
articleContentProcessorServiceProvider,
(previous, next) {},
);
}
@override
@@ -68,8 +68,8 @@ class _PreferenceRepository extends _$PreferenceRepository {
Raw<Stream<Map<String, Object>>> build() {
_prefSubject = BehaviorSubject<Map<String, Object>>();
ref.onDispose(() {
unawaited(_prefSubject.close());
ref.onDispose(() async {
await _prefSubject.close();
});
ref.onAddListener(() async {
@@ -330,7 +330,7 @@ class _PreferenceSettingGroupProviderElement
}
String _$preferenceRepositoryHash() =>
r'53c0cbbf60e44c3834fe3e3255836f7e27f09c7f';
r'b2b7f75b3d30842b3006bb18af89f7ff90ba4ce7';
/// See also [_PreferenceRepository].
@ProviderFor(_PreferenceRepository)
@@ -9,6 +9,7 @@ import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_field.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/feed_search.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/search_suggestions.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart';
@@ -116,6 +117,7 @@ class SearchScreen extends HookConsumerWidget {
),
const SliverToBoxAdapter(child: Divider()),
TabSearch(searchTextController: searchTextController),
FeedSearch(searchTextController: searchTextController),
HistorySuggestions(searchTextController: searchTextController),
],
),
@@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/extensions/uri.dart';
import 'package:lensai/features/web_feed/data/models/feed_article_query_result.dart';
import 'package:lensai/features/web_feed/data/models/feed_link.dart';
import 'package:lensai/features/web_feed/domain/providers.dart';
import 'package:lensai/features/web_feed/extensions/atom.dart';
import 'package:lensai/features/web_feed/extensions/feed_article.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart';
import 'package:lensai/presentation/widgets/url_icon.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:sliver_tools/sliver_tools.dart';
class FeedSearch extends HookConsumerWidget {
static const _matchPrefix = '***';
static const _matchSuffix = '***';
final TextEditingController searchTextController;
const FeedSearch({required this.searchTextController});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final articlesAsync = ref.watch(articleSearchProvider(null));
useListenableCallback(searchTextController, () async {
await ref
.read(articleSearchProvider(null).notifier)
.search(
searchTextController.text,
// ignore: avoid_redundant_argument_values dont break things
matchPrefix: _matchPrefix,
// ignore: avoid_redundant_argument_values dont break things
matchSuffix: _matchSuffix,
);
});
if (articlesAsync.hasValue && (articlesAsync.valueOrNull.isEmpty)) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return MultiSliver(
children: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Articles', style: Theme.of(context).textTheme.labelSmall),
],
),
),
),
SliverSkeletonizer(
enabled: articlesAsync.isLoading,
child: articlesAsync.when(
data:
(articles) => SliverList.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
final article = articles[index];
final titleHighlight = switch (article) {
final FeedArticleQueryResult result =>
result.titleHighlight.whenNotEmpty,
_ => null,
};
final searchSnippet = switch (article) {
final FeedArticleQueryResult result =>
result.summarySnippet.whenNotEmpty ??
result.contentSnippet.whenNotEmpty,
_ => null,
};
return ListTile(
leading: RepaintBoundary(
child: UrlIcon([
article.icon ??
article.links
?.getRelation(FeedLinkRelation.alternate)
?.uri ??
article.siteLink ??
article.feedId.base,
], iconSize: 24.0),
),
title:
(titleHighlight.isNotEmpty)
? MarkdownBody(
data: titleHighlight!,
styleSheet: MarkdownStyleSheet(
p: Theme.of(
context,
).textTheme.titleMedium?.copyWith(
color:
Theme.of(context).colorScheme.onSurface,
),
),
)
: Text(
article.displayTitle,
style: theme.textTheme.titleMedium,
),
subtitle:
(searchSnippet.isNotEmpty)
? MarkdownBody(
data: searchSnippet!,
styleSheet: MarkdownStyleSheet(
p: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
a: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
decoration: TextDecoration.none,
),
),
)
: ((article.summaryPlain != null)
? Text(
article.summaryPlain!,
style: theme.textTheme.bodySmall,
maxLines: 3,
overflow: TextOverflow.ellipsis,
)
: null),
onTap: () {
FeedArticleRoute(
articleId: article.id,
).pushReplacement(context);
},
);
},
),
error: (error, stackTrace) {
return SliverToBoxAdapter(
child: FailureWidget(
title: 'Failed searching Articles',
exception: error,
),
);
},
loading:
() => SliverList.builder(
itemCount: articlesAsync.valueOrNull?.length ?? 3,
itemBuilder: (context, index) {
return const ListTile(title: Bone.text());
},
),
),
),
],
);
}
}
@@ -20,7 +20,7 @@ class HistorySuggestions extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final historySuggestions = ref.watch(engineHistorySuggestionsProvider);
final historySuggestionsAsync = ref.watch(engineHistorySuggestionsProvider);
useListenableCallback(searchTextController, () async {
await ref
@@ -28,8 +28,8 @@ class HistorySuggestions extends HookConsumerWidget {
.addQuery(searchTextController.text);
});
if (historySuggestions.hasValue &&
(historySuggestions.valueOrNull.isEmpty)) {
if (historySuggestionsAsync.hasValue &&
(historySuggestionsAsync.valueOrNull.isEmpty)) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
@@ -46,8 +46,8 @@ class HistorySuggestions extends HookConsumerWidget {
),
),
SliverSkeletonizer(
enabled: historySuggestions.isLoading,
child: historySuggestions.when(
enabled: historySuggestionsAsync.isLoading,
child: historySuggestionsAsync.when(
skipLoadingOnReload: true,
data: (historySuggestions) {
return SliverList.builder(
@@ -109,14 +109,16 @@ class HistorySuggestions extends HookConsumerWidget {
);
},
error: (error, stackTrace) {
return FailureWidget(
title: 'Could not load history',
exception: error,
return SliverToBoxAdapter(
child: FailureWidget(
title: 'Could not load history',
exception: error,
),
);
},
loading:
() => SliverList.builder(
itemCount: historySuggestions.valueOrNull?.length ?? 3,
itemCount: historySuggestionsAsync.valueOrNull?.length ?? 3,
itemBuilder: (context, index) {
return const ListTile(title: Bone.text());
},
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:lensai/core/providers/models.dart';
@@ -51,8 +49,8 @@ TabDatabase tabDatabase(Ref ref) {
embeddingDimensions: dimensions,
);
ref.onDispose(() {
unawaited(db.close());
ref.onDispose(() async {
await db.close();
});
return db;
@@ -6,7 +6,7 @@ part of 'providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$tabDatabaseHash() => r'940dd2a1f1df6f2a0b77a1a3e9b8318550b832d2';
String _$tabDatabaseHash() => r'db190f01d7e70e65b9f44403effa82ec503144a0';
/// See also [tabDatabase].
@ProviderFor(tabDatabase)
@@ -108,8 +108,8 @@ class DocumentRepository extends _$DocumentRepository {
}
});
ref.onDispose(() {
unawaited(tabContentSub.cancel());
ref.onDispose(() async {
await tabContentSub.cancel();
});
}
}
@@ -7,7 +7,7 @@ part of 'document.dart';
// **************************************************************************
String _$documentRepositoryHash() =>
r'60e5a0c889d9659c8e0e254c59225c2041a0cb56';
r'fd78b5eaf96951724e905b068144dcf2a254dcbb';
/// See also [DocumentRepository].
@ProviderFor(DocumentRepository)