major update

This commit is contained in:
Fabian Freund
2025-03-03 15:15:05 +01:00
parent 200c82a442
commit 20bdc4df3e
103 changed files with 2664 additions and 783 deletions
@@ -29,7 +29,7 @@ GeckoSelectionActionService selectionActionService(Ref ref) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: defaultSearchBang.getUrl(text),
url: defaultSearchBang.getTemplateUrl(text),
parentId: currentTabId,
);
} else {
@@ -7,7 +7,7 @@ part of 'providers.dart';
// **************************************************************************
String _$selectionActionServiceHash() =>
r'8d0af53213bdb924bcf5f8df6ceec6c81a345345';
r'd907a3c5ab7efde82bb2cc1fb0409fd3fd13bb25';
/// See also [selectionActionService].
@ProviderFor(selectionActionService)
@@ -0,0 +1,22 @@
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'browser_extension.g.dart';
@Riverpod(keepAlive: true)
GeckoBrowserExtensionService browserExtensionService(Ref ref) {
final service = GeckoBrowserExtensionService.setUp();
ref.onDispose(() {
service.dispose();
});
return service;
}
@Riverpod()
Stream<String> feedRequested(Ref ref) {
final service = ref.watch(browserExtensionServiceProvider);
return service.feedRequested;
}
@@ -0,0 +1,48 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'browser_extension.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$browserExtensionServiceHash() =>
r'c3f67763e0abb10039b407fced6e175e5ec7c6c3';
/// See also [browserExtensionService].
@ProviderFor(browserExtensionService)
final browserExtensionServiceProvider =
Provider<GeckoBrowserExtensionService>.internal(
browserExtensionService,
name: r'browserExtensionServiceProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$browserExtensionServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef BrowserExtensionServiceRef = ProviderRef<GeckoBrowserExtensionService>;
String _$feedRequestedHash() => r'4f179d0878072a77a87422ff6afdac53a3d57c04';
/// See also [feedRequested].
@ProviderFor(feedRequested)
final feedRequestedProvider = AutoDisposeStreamProvider<String>.internal(
feedRequested,
name: r'feedRequestedProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$feedRequestedHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef FeedRequestedRef = AutoDisposeStreamProviderRef<String>;
// 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
@@ -162,7 +162,7 @@ class TabRepository extends _$TabRepository {
ref.read(selectedBangDataProvider()) ??
await ref.read(defaultSearchBangDataProvider.future);
await addTab(url: defaultSearchBang?.getUrl(value.text));
await addTab(url: defaultSearchBang?.getTemplateUrl(value.text));
}
});
});
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator
// **************************************************************************
String _$tabRepositoryHash() => r'12e4fb7e9bf36a6df9f0a29df4b410f051d5c38e';
String _$tabRepositoryHash() => r'69e283b820d86388f4d80e71e08761e2b6890507';
/// See also [TabRepository].
@ProviderFor(TabRepository)
@@ -81,6 +81,7 @@ class WebPageDialog extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: availableBangsAsync.when(
skipLoadingOnReload: true,
data: (availableBangs) {
if (availableBangs.isEmpty) {
return const SizedBox.shrink();
@@ -53,10 +53,7 @@ class BrowserScreen extends HookConsumerWidget {
useOnStreamChange(
eventService.longPressEvent,
onData: (event) async {
await context.push(
const ContextMenuRoute().location,
extra: event.hitResult.toJson(),
);
await ContextMenuRoute(event.hitResult.toJson()).push(context);
},
);
@@ -66,7 +66,7 @@ class AddressWithSuggestionsField extends HookConsumerWidget {
defaultSearchBangDataProvider.future,
);
newUrl = defaultSearchBang?.getUrl(value);
newUrl = defaultSearchBang?.getTemplateUrl(value);
}
if (newUrl != null) {
@@ -17,7 +17,6 @@ import 'package:lensai/features/geckoview/features/browser/presentation/widgets/
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
import 'package:lensai/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart';
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
import 'package:lensai/features/web_feed/domain/services/feed_reader.dart';
import 'package:lensai/presentation/hooks/menu_controller.dart';
import 'package:lensai/presentation/icons/tor_icons.dart';
import 'package:lensai/utils/ui_helper.dart' as ui_helper;
@@ -55,12 +54,10 @@ class BrowserBottomAppBar extends HookConsumerWidget {
? AppBarTitle(
tab: tabState,
onTap: () async {
await context.push(
WebPageRoute(
url: tabState.url.toString(),
).location,
extra: tabState,
);
await WebPageRoute(
url: tabState.url.toString(),
$extra: tabState,
).push(context);
},
onLongPress: () async {
final newUrl = await showDialog<Uri?>(
@@ -104,7 +101,7 @@ class BrowserBottomAppBar extends HookConsumerWidget {
),
MenuItemButton(
onPressed: () async {
await context.push(const SearchRoute().location);
await const SearchRoute().push(context);
},
leadingIcon: const Icon(Icons.add),
child: const Text('Add Tab'),
@@ -183,7 +180,7 @@ class BrowserBottomAppBar extends HookConsumerWidget {
),
MenuItemButton(
onPressed: () async {
await context.push(AboutRoute().location);
await AboutRoute().push(context);
},
leadingIcon: const Icon(Icons.info),
child: const Text('About'),
@@ -191,7 +188,7 @@ class BrowserBottomAppBar extends HookConsumerWidget {
const Divider(),
MenuItemButton(
onPressed: () async {
await context.push(SettingsRoute().location);
await SettingsRoute().push(context);
},
leadingIcon: const Icon(Icons.settings),
child: const Text('Settings'),
@@ -234,7 +231,7 @@ class BrowserBottomAppBar extends HookConsumerWidget {
),
MenuItemButton(
onPressed: () async {
await context.push(TorProxyRoute().location);
await TorProxyRoute().push(context);
},
leadingIcon: const Icon(TorIcons.onionAlt),
child: const Text('Tor'),
@@ -242,11 +239,18 @@ class BrowserBottomAppBar extends HookConsumerWidget {
const Divider(),
MenuItemButton(
onPressed: () async {
await context.push(BangCategoriesRoute().location);
await BangCategoriesRoute().push(context);
},
leadingIcon: const Icon(MdiIcons.exclamationThick),
child: const Text('Bangs'),
),
MenuItemButton(
onPressed: () async {
await context.push(FeedListRoute().location);
},
leadingIcon: const Icon(Icons.rss_feed),
child: const Text('Feeds'),
),
const Divider(),
if (selectedTabId != null)
MenuItemButton(
@@ -358,31 +362,6 @@ class BrowserBottomAppBar extends HookConsumerWidget {
);
},
),
MenuItemButton(
onPressed: () async {
final res = await ref
.read(feedReaderProvider.notifier)
.parseFeed(
Uri.parse('https://simonwillison.net/atom/everything/'),
);
if (context.mounted) {
await context.push(
FeedCreateRoute().location,
extra: res.feedData,
);
}
},
leadingIcon: const Icon(Icons.info),
child: const Text('Add'),
),
MenuItemButton(
onPressed: () async {
await context.push(FeedListRoute().location);
},
leadingIcon: const Icon(Icons.info),
child: const Text('List'),
),
],
),
],
@@ -4,7 +4,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/logger.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/domain/providers/browser_extension.dart';
import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
@@ -78,6 +81,12 @@ class _BrowserViewState extends ConsumerState<BrowserView>
},
);
ref.listen(feedRequestedProvider, (previous, next) async {
if (next.valueOrNull.mapNotNull(Uri.tryParse) case final Uri url) {
await FeedAddRoute($extra: url).push(context);
}
});
return Visibility(
visible: hasTab,
replacement: SizedBox.expand(child: Container(color: Colors.grey[800])),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lensai/utils/form_validators.dart';
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
class EditUrlDialog extends HookWidget {
@@ -22,13 +23,7 @@ class EditUrlDialog extends HookWidget {
keyboardType: TextInputType.url,
decoration: const InputDecoration(hintText: 'Enter URL'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a URL';
}
if (uri_parser.tryParseUrl(value) == null) {
return 'Please enter a valid URL';
}
return null;
return validateUrl(value, requireAuthority: false);
},
),
),
@@ -40,7 +35,9 @@ class EditUrlDialog extends HookWidget {
TextButton(
onPressed: () {
if (formKey.currentState?.validate() ?? false) {
Navigator.of(context).pop(Uri.parse(urlController.text));
Navigator.of(context).pop(
uri_parser.tryParseUrl(urlController.text, eagerParsing: true),
);
}
},
child: const Text('Edit'),
@@ -4,7 +4,6 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_reorderable_grid_view/widgets/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/providers/global_drop.dart';
import 'package:lensai/core/routing/routes.dart';
@@ -376,7 +375,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
),
child: FloatingActionButton.small(
onPressed: () async {
await context.push(const SearchRoute().location);
await const SearchRoute().push(context);
onClose();
},
@@ -45,6 +45,7 @@ class ReaderButton extends HookConsumerWidget {
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15.0, horizontal: 8.0),
child: readerChanging.when(
skipLoadingOnReload: true,
data:
(_) => Visibility(
visible: readerabilityState.readerable,
@@ -86,7 +86,7 @@ class SearchScreen extends HookConsumerWidget {
ref.read(selectedBangDataProvider()) ??
await ref.read(defaultSearchBangDataProvider.future);
newUrl = defaultSearchBang?.getUrl(value);
newUrl = defaultSearchBang?.getTemplateUrl(value);
}
if (newUrl != null) {
@@ -1,5 +1,4 @@
import 'package:flutter/material.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/extensions/nullable.dart';
@@ -52,15 +51,16 @@ class BangChips extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final availableBangs = ref.watch(seamlessBangProviderProvider);
final availableBangs = ref.watch(seamlessBangProvider);
useListenableCallback(searchTextController, () {
ref
.read(seamlessBangProviderProvider.notifier)
.read(seamlessBangProvider.notifier)
.search(searchTextController!.text);
});
return availableBangs.when(
skipLoadingOnReload: true,
data: (availableBangs) {
return SizedBox(
height: 48,
@@ -71,7 +71,8 @@ class BangChips extends HookConsumerWidget {
child: SelectableChips(
itemId: (bang) => bang.trigger,
itemAvatar:
(bang) => UrlIcon(bang.getUrl(''), iconSize: 20),
(bang) =>
UrlIcon([bang.getTemplateUrl('')], iconSize: 20),
itemLabel: (bang) => Text(bang.websiteName),
availableItems: availableBangs,
selectedItem: activeBang,
@@ -97,14 +98,12 @@ class BangChips extends HookConsumerWidget {
onPressed: () async {
final searchText = searchTextController?.text.trim();
await context.push(
BangSearchRoute(
searchText:
(searchText.isEmpty)
? BangSearchRoute.emptySearchText
: searchText!,
).location,
);
await BangSearchRoute(
searchText:
(searchText.isEmpty)
? BangSearchRoute.emptySearchText
: searchText!,
).push(context);
},
icon: const Icon(Icons.chevron_right),
),
@@ -67,7 +67,9 @@ class SearchField extends HookConsumerWidget {
(showBangIcon && activeBang != null)
? Padding(
padding: const EdgeInsetsDirectional.all(12.0),
child: UrlIcon(activeBang!.getUrl(''), iconSize: 24.0),
child: UrlIcon([
activeBang!.getTemplateUrl(''),
], iconSize: 24.0),
)
: null,
label: label ?? const Text('Search'),
@@ -48,6 +48,7 @@ class HistorySuggestions extends HookConsumerWidget {
SliverSkeletonizer(
enabled: historySuggestions.isLoading,
child: historySuggestions.when(
skipLoadingOnReload: true,
data: (historySuggestions) {
return SliverList.builder(
itemCount: historySuggestions.length,
@@ -96,13 +96,11 @@ class TabSearch extends HookConsumerWidget {
return ListTile(
leading: RepaintBoundary(
child:
(result.icon != null)
? RawImage(
image: result.icon?.value,
height: 24,
width: 24,
)
: UrlIcon(result.url, iconSize: 24),
result.icon.mapNotNull(
(icon) =>
RawImage(image: icon.value, height: 24, width: 24),
) ??
UrlIcon([result.url], iconSize: 24),
),
title: result.title.mapNotNull(
(title) => MarkdownBody(
@@ -226,11 +226,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
ellipsis: ellipsis,
);
} else {
return db.queryTabsBasic(
query: db.buildLikeQuery(searchString),
beforeMatch: matchPrefix,
afterMatch: matchSuffix,
);
return db.queryTabsBasic(query: db.buildLikeQuery(searchString));
}
}
}
@@ -126,10 +126,10 @@ queryTabsBasic WITH TabQueryResult:
)
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,
t.url AS clean_url
t.title,
CAST(t.url AS TEXT) AS url,
t.url AS clean_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
@@ -156,11 +156,11 @@ queryTabsFullContent WITH TabQueryResult:
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,
t.url AS clean_url,
(
bm25(tab_fts, weights.title_weight, weights.url_weight,
weights.extracted_weight, weights.full_weight)
) AS weighted_rank,
t.url AS clean_url
) AS weighted_rank
FROM tab_fts(:query) fts
INNER JOIN
tab t ON t.rowid = fts.rowid
@@ -1901,18 +1901,10 @@ abstract class _$TabDatabase extends GeneratedDatabase {
).map((QueryRow row) => row.read<String>('_c0'));
}
Selectable<TabQueryResult> queryTabsBasic({
required String beforeMatch,
required String afterMatch,
required String query,
}) {
Selectable<TabQueryResult> queryTabsBasic({required String query}) {
return customSelect(
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank, t.url AS clean_url FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?3 OR fts.url LIKE ?3 ORDER BY weighted_rank ASC, t.timestamp DESC',
variables: [
Variable<String>(beforeMatch),
Variable<String>(afterMatch),
Variable<String>(query),
],
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, t.title, CAST(t.url AS TEXT) AS url, t.url AS clean_url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?1 OR fts.url LIKE ?1 ORDER BY weighted_rank ASC, t.timestamp DESC',
variables: [Variable<String>(query)],
readsFrom: {tab, tabFts},
).map(
(QueryRow row) => TabQueryResult(
@@ -1936,7 +1928,7 @@ abstract class _$TabDatabase extends GeneratedDatabase {
required String query,
}) {
return customSelect(
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank, t.url AS clean_url FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC',
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content, t.url AS clean_url,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC',
variables: [
Variable<String>(beforeMatch),
Variable<String>(afterMatch),
@@ -1,7 +1,6 @@
import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.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/core/uuid.dart';
@@ -28,6 +27,7 @@ class ContainerListScreen extends HookConsumerWidget {
return Skeletonizer(
enabled: containersAsync.isLoading,
child: containersAsync.when(
skipLoadingOnReload: true,
data:
(containers) => FadingScroll(
fadingSize: 25,
@@ -115,10 +115,9 @@ class ContainerListScreen extends HookConsumerWidget {
.unusedRandomContainerColor();
if (context.mounted) {
final result = await context.push<ContainerData?>(
ContainerCreateRoute().location,
extra: ContainerData(id: uuid.v7(), color: initialColor),
);
final result = await ContainerCreateRoute(
ContainerData(id: uuid.v7(), color: initialColor),
).push<ContainerData?>(context);
if (result != null) {
await ref
@@ -1,6 +1,5 @@
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/providers/global_drop.dart';
import 'package:lensai/core/routing/routes.dart';
@@ -201,7 +200,7 @@ class ContainerChips extends HookConsumerWidget {
if (displayMenu)
IconButton(
onPressed: () async {
await context.push(ContainerListRoute().location);
await ContainerListRoute().push(context);
},
icon: const Icon(Icons.chevron_right),
),
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
@@ -28,7 +27,7 @@ class ContainerListTile extends HookWidget {
title: Text(container.name ?? 'New Container'),
trailing: IconButton(
onPressed: () async {
await context.push(ContainerEditRoute().location, extra: container);
await ContainerEditRoute(container).push(context);
},
icon: const Icon(Icons.chevron_right),
),