major update
This commit is contained in:
@@ -36,17 +36,14 @@ class BrowserRoute extends GoRouteData {
|
||||
|
||||
class WebPageRoute extends GoRouteData {
|
||||
final String url;
|
||||
final WebPageInfo? $extra;
|
||||
|
||||
const WebPageRoute({required this.url});
|
||||
const WebPageRoute({required this.url, required this.$extra});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(
|
||||
builder:
|
||||
(_) => WebPageDialog(
|
||||
url: Uri.parse(url),
|
||||
precachedInfo: state.extra as WebPageInfo?,
|
||||
),
|
||||
builder: (_) => WebPageDialog(url: Uri.parse(url), precachedInfo: $extra),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -85,33 +82,37 @@ class ContainerListRoute extends GoRouteData {
|
||||
}
|
||||
|
||||
class ContainerEditRoute extends GoRouteData {
|
||||
final ContainerData $extra;
|
||||
|
||||
ContainerEditRoute(this.$extra);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ContainerEditScreen.edit(
|
||||
initialContainer: state.extra! as ContainerData,
|
||||
);
|
||||
return ContainerEditScreen.edit(initialContainer: $extra);
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerCreateRoute extends GoRouteData {
|
||||
final ContainerData $extra;
|
||||
|
||||
ContainerCreateRoute(this.$extra);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ContainerEditScreen.create(
|
||||
initialContainer: state.extra! as ContainerData,
|
||||
);
|
||||
return ContainerEditScreen.create(initialContainer: $extra);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextMenuRoute extends GoRouteData {
|
||||
const ContextMenuRoute();
|
||||
final String $extra;
|
||||
|
||||
const ContextMenuRoute(this.$extra);
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(
|
||||
builder:
|
||||
(_) => ContextMenuDialog(
|
||||
hitResult: HitResultJson.fromJson(state.extra! as String),
|
||||
),
|
||||
(_) => ContextMenuDialog(hitResult: HitResultJson.fromJson($extra)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lensai/core/routing/widgets/dialog_page.dart';
|
||||
@@ -22,11 +24,12 @@ import 'package:lensai/features/settings/presentation/screens/web_engine_hardeni
|
||||
import 'package:lensai/features/settings/presentation/screens/web_engine_settings.dart';
|
||||
import 'package:lensai/features/tor/presentation/screens/tor_proxy.dart';
|
||||
import 'package:lensai/features/user/presentation/screens/auth.dart';
|
||||
import 'package:lensai/features/web_feed/data/database/database.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/add_feed_dialog.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/screens/feed_article.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/screens/feed_article_list.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/screens/feed_edit.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/screens/feed_list.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/select_feed_dialog.dart';
|
||||
|
||||
part 'routes.g.dart';
|
||||
part 'routes.settings.dart';
|
||||
|
||||
@@ -4,6 +4,7 @@ part of 'routes.dart';
|
||||
name: 'FeedListRoute',
|
||||
path: '/feeds',
|
||||
routes: [
|
||||
TypedGoRoute<FeedAddRoute>(name: 'FeedAddRoute', path: 'add'),
|
||||
TypedGoRoute<FeedArticleListRoute>(
|
||||
name: 'FeedArticleListRoute',
|
||||
path: 'articles/:feedId',
|
||||
@@ -12,7 +13,15 @@ part of 'routes.dart';
|
||||
name: 'FeedArticleRoute',
|
||||
path: 'article/:articleId',
|
||||
),
|
||||
TypedGoRoute<FeedCreateRoute>(name: 'FeedCreateRoute', path: 'create'),
|
||||
TypedGoRoute<FeedCreateRoute>(
|
||||
name: 'FeedCreateRoute',
|
||||
path: 'create/:feedId',
|
||||
),
|
||||
TypedGoRoute<SelectFeedDialogRoute>(
|
||||
name: 'SelectFeedDialogRoute',
|
||||
path: 'available/:feedsJson',
|
||||
),
|
||||
TypedGoRoute<FeedEditRoute>(name: 'FeedEditRoute', path: 'edit/:feedId'),
|
||||
],
|
||||
)
|
||||
class FeedListRoute extends GoRouteData {
|
||||
@@ -23,9 +32,52 @@ class FeedListRoute extends GoRouteData {
|
||||
}
|
||||
|
||||
class FeedCreateRoute extends GoRouteData {
|
||||
final Uri feedId;
|
||||
|
||||
FeedCreateRoute({required this.feedId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedEditScreen.create(initialFeed: state.extra! as FeedData);
|
||||
return FeedEditScreen.create(feedId: feedId);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectFeedDialogRoute extends GoRouteData {
|
||||
final String feedsJson;
|
||||
|
||||
const SelectFeedDialogRoute({required this.feedsJson});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
final feedUris = Set<Uri>.from(
|
||||
(jsonDecode(feedsJson) as List<dynamic>).map(
|
||||
(url) => Uri.parse(url as String),
|
||||
),
|
||||
);
|
||||
|
||||
return DialogPage(builder: (_) => SelectFeedDialog(feedUris: feedUris));
|
||||
}
|
||||
}
|
||||
|
||||
class FeedEditRoute extends GoRouteData {
|
||||
final Uri feedId;
|
||||
|
||||
const FeedEditRoute({required this.feedId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedEditScreen.edit(feedId: feedId);
|
||||
}
|
||||
}
|
||||
|
||||
class FeedAddRoute extends GoRouteData {
|
||||
final Uri? $extra;
|
||||
|
||||
const FeedAddRoute({this.$extra});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(builder: (_) => AddFeedDialog(initialUri: $extra));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -270,20 +270,24 @@ extension $BrowserRouteExtension on BrowserRoute {
|
||||
}
|
||||
|
||||
extension $WebPageRouteExtension on WebPageRoute {
|
||||
static WebPageRoute _fromState(GoRouterState state) =>
|
||||
WebPageRoute(url: state.pathParameters['url']!);
|
||||
static WebPageRoute _fromState(GoRouterState state) => WebPageRoute(
|
||||
url: state.pathParameters['url']!,
|
||||
$extra: state.extra as WebPageInfo?,
|
||||
);
|
||||
|
||||
String get location =>
|
||||
GoRouteData.$location('/page/${Uri.encodeComponent(url)}');
|
||||
|
||||
void go(BuildContext context) => context.go(location);
|
||||
void go(BuildContext context) => context.go(location, extra: $extra);
|
||||
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: $extra);
|
||||
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
context.pushReplacement(location, extra: $extra);
|
||||
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: $extra);
|
||||
}
|
||||
|
||||
extension $SearchRouteExtension on SearchRoute {
|
||||
@@ -322,18 +326,20 @@ extension $TorProxyRouteExtension on TorProxyRoute {
|
||||
|
||||
extension $ContextMenuRouteExtension on ContextMenuRoute {
|
||||
static ContextMenuRoute _fromState(GoRouterState state) =>
|
||||
const ContextMenuRoute();
|
||||
ContextMenuRoute(state.extra as String);
|
||||
|
||||
String get location => GoRouteData.$location('/context_menu');
|
||||
|
||||
void go(BuildContext context) => context.go(location);
|
||||
void go(BuildContext context) => context.go(location, extra: $extra);
|
||||
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: $extra);
|
||||
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
context.pushReplacement(location, extra: $extra);
|
||||
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: $extra);
|
||||
}
|
||||
|
||||
extension $ContainerListRouteExtension on ContainerListRoute {
|
||||
@@ -354,34 +360,38 @@ extension $ContainerListRouteExtension on ContainerListRoute {
|
||||
|
||||
extension $ContainerCreateRouteExtension on ContainerCreateRoute {
|
||||
static ContainerCreateRoute _fromState(GoRouterState state) =>
|
||||
ContainerCreateRoute();
|
||||
ContainerCreateRoute(state.extra as ContainerData);
|
||||
|
||||
String get location => GoRouteData.$location('/containers/create');
|
||||
|
||||
void go(BuildContext context) => context.go(location);
|
||||
void go(BuildContext context) => context.go(location, extra: $extra);
|
||||
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: $extra);
|
||||
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
context.pushReplacement(location, extra: $extra);
|
||||
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: $extra);
|
||||
}
|
||||
|
||||
extension $ContainerEditRouteExtension on ContainerEditRoute {
|
||||
static ContainerEditRoute _fromState(GoRouterState state) =>
|
||||
ContainerEditRoute();
|
||||
ContainerEditRoute(state.extra as ContainerData);
|
||||
|
||||
String get location => GoRouteData.$location('/containers/edit');
|
||||
|
||||
void go(BuildContext context) => context.go(location);
|
||||
void go(BuildContext context) => context.go(location, extra: $extra);
|
||||
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: $extra);
|
||||
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
context.pushReplacement(location, extra: $extra);
|
||||
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: $extra);
|
||||
}
|
||||
|
||||
RouteBase get $bangCategoriesRoute => GoRouteData.$route(
|
||||
@@ -492,6 +502,12 @@ RouteBase get $feedListRoute => GoRouteData.$route(
|
||||
|
||||
factory: $FeedListRouteExtension._fromState,
|
||||
routes: [
|
||||
GoRouteData.$route(
|
||||
path: 'add',
|
||||
name: 'FeedAddRoute',
|
||||
|
||||
factory: $FeedAddRouteExtension._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'articles/:feedId',
|
||||
name: 'FeedArticleListRoute',
|
||||
@@ -505,11 +521,23 @@ RouteBase get $feedListRoute => GoRouteData.$route(
|
||||
factory: $FeedArticleRouteExtension._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'create',
|
||||
path: 'create/:feedId',
|
||||
name: 'FeedCreateRoute',
|
||||
|
||||
factory: $FeedCreateRouteExtension._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'available/:feedsJson',
|
||||
name: 'SelectFeedDialogRoute',
|
||||
|
||||
factory: $SelectFeedDialogRouteExtension._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'edit/:feedId',
|
||||
name: 'FeedEditRoute',
|
||||
|
||||
factory: $FeedEditRouteExtension._fromState,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -528,6 +556,24 @@ extension $FeedListRouteExtension on FeedListRoute {
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
extension $FeedAddRouteExtension on FeedAddRoute {
|
||||
static FeedAddRoute _fromState(GoRouterState state) =>
|
||||
FeedAddRoute($extra: state.extra as Uri?);
|
||||
|
||||
String get location => GoRouteData.$location('/feeds/add');
|
||||
|
||||
void go(BuildContext context) => context.go(location, extra: $extra);
|
||||
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: $extra);
|
||||
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location, extra: $extra);
|
||||
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: $extra);
|
||||
}
|
||||
|
||||
extension $FeedArticleListRouteExtension on FeedArticleListRoute {
|
||||
static FeedArticleListRoute _fromState(GoRouterState state) =>
|
||||
FeedArticleListRoute(feedId: Uri.parse(state.pathParameters['feedId']!)!);
|
||||
@@ -564,9 +610,48 @@ extension $FeedArticleRouteExtension on FeedArticleRoute {
|
||||
}
|
||||
|
||||
extension $FeedCreateRouteExtension on FeedCreateRoute {
|
||||
static FeedCreateRoute _fromState(GoRouterState state) => FeedCreateRoute();
|
||||
static FeedCreateRoute _fromState(GoRouterState state) =>
|
||||
FeedCreateRoute(feedId: Uri.parse(state.pathParameters['feedId']!)!);
|
||||
|
||||
String get location => GoRouteData.$location('/feeds/create');
|
||||
String get location => GoRouteData.$location(
|
||||
'/feeds/create/${Uri.encodeComponent(feedId.toString())}',
|
||||
);
|
||||
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
extension $SelectFeedDialogRouteExtension on SelectFeedDialogRoute {
|
||||
static SelectFeedDialogRoute _fromState(GoRouterState state) =>
|
||||
SelectFeedDialogRoute(feedsJson: state.pathParameters['feedsJson']!);
|
||||
|
||||
String get location => GoRouteData.$location(
|
||||
'/feeds/available/${Uri.encodeComponent(feedsJson)}',
|
||||
);
|
||||
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
extension $FeedEditRouteExtension on FeedEditRoute {
|
||||
static FeedEditRoute _fromState(GoRouterState state) =>
|
||||
FeedEditRoute(feedId: Uri.parse(state.pathParameters['feedId']!)!);
|
||||
|
||||
String get location => GoRouteData.$location(
|
||||
'/feeds/edit/${Uri.encodeComponent(feedId.toString())}',
|
||||
);
|
||||
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class WebPageInfo with FastEquatable {
|
||||
final Uri url;
|
||||
final String? title;
|
||||
final BrowserIcon? favicon;
|
||||
final Set<String>? feeds;
|
||||
final Set<Uri>? feeds;
|
||||
|
||||
bool get isPageInfoComplete =>
|
||||
title.isNotEmpty && favicon != null && feeds != null;
|
||||
|
||||
@@ -13,7 +13,7 @@ abstract class _$WebPageInfoCWProxy {
|
||||
|
||||
WebPageInfo favicon(BrowserIcon? favicon);
|
||||
|
||||
WebPageInfo feeds(Set<String>? feeds);
|
||||
WebPageInfo feeds(Set<Uri>? feeds);
|
||||
|
||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebPageInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||
///
|
||||
@@ -25,7 +25,7 @@ abstract class _$WebPageInfoCWProxy {
|
||||
Uri url,
|
||||
String? title,
|
||||
BrowserIcon? favicon,
|
||||
Set<String>? feeds,
|
||||
Set<Uri>? feeds,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class _$WebPageInfoCWProxyImpl implements _$WebPageInfoCWProxy {
|
||||
WebPageInfo favicon(BrowserIcon? favicon) => this(favicon: favicon);
|
||||
|
||||
@override
|
||||
WebPageInfo feeds(Set<String>? feeds) => this(feeds: feeds);
|
||||
WebPageInfo feeds(Set<Uri>? feeds) => this(feeds: feeds);
|
||||
|
||||
@override
|
||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebPageInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||
@@ -80,7 +80,7 @@ class _$WebPageInfoCWProxyImpl implements _$WebPageInfoCWProxy {
|
||||
feeds == const $CopyWithPlaceholder()
|
||||
? _value.feeds
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: feeds as Set<String>?,
|
||||
: feeds as Set<Uri>?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +186,11 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
||||
return icons;
|
||||
}
|
||||
|
||||
Future<Result<WebPageInfo>> fetchPageInfo(Uri url) {
|
||||
Future<Result<WebPageInfo>> fetchPageInfo(Uri url, bool isImageRequest) {
|
||||
return Result.fromAsync(() async {
|
||||
final result = await compute((String urlString) async {
|
||||
final result = await compute((args) async {
|
||||
final [String urlString, bool isImageRequest] = args;
|
||||
|
||||
final client = http.Client();
|
||||
try {
|
||||
final baseUri = Uri.parse(urlString);
|
||||
@@ -196,6 +198,16 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
||||
.get(baseUri)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
|
||||
//When this is a request for an icon and we hit an image, directly return it
|
||||
if (isImageRequest) {
|
||||
final contentType = response.headers['content-type'];
|
||||
if (contentType?.contains('image/') == true) {
|
||||
return {
|
||||
'imageBytes': [response.bodyBytes],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final document = html_parser.parse(response.body);
|
||||
|
||||
final title = document.querySelector('title')?.text;
|
||||
@@ -206,12 +218,23 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
||||
return {
|
||||
'title': title,
|
||||
'resources': resources.map(_serializeResource).toList(),
|
||||
'feeds': feeds,
|
||||
'feeds': feeds.map((uri) => uri.toString()).toList(),
|
||||
};
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}, url.toString());
|
||||
}, <dynamic>[url.toString(), isImageRequest]);
|
||||
|
||||
if (result['imageBytes'] case final Uint8List imageBytes) {
|
||||
return WebPageInfo(
|
||||
url: url,
|
||||
favicon: await BrowserIcon.fromBytes(
|
||||
imageBytes,
|
||||
dominantColor: null,
|
||||
source: IconSource.download,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final resources =
|
||||
(result['resources']! as List<Map<String, dynamic>>)
|
||||
@@ -226,7 +249,9 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
||||
url: url,
|
||||
title: result['title'] as String?,
|
||||
favicon: favicon,
|
||||
feeds: result['feeds'] as Set<String>?,
|
||||
feeds: Set.from(
|
||||
(result['feeds']! as List<String>).map((url) => Uri.tryParse(url)),
|
||||
),
|
||||
);
|
||||
}, exceptionHandler: handleHttpError);
|
||||
}
|
||||
@@ -277,17 +302,29 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<BrowserIcon> getUrlIcon(Uri url) async {
|
||||
final cachedIcon = await getCachedIcon(url);
|
||||
Future<BrowserIcon?> getUrlIcon(List<Uri> urlList) async {
|
||||
for (final url in urlList) {
|
||||
final cachedIcon = await getCachedIcon(url);
|
||||
|
||||
if (cachedIcon != null) {
|
||||
return cachedIcon;
|
||||
if (cachedIcon != null) {
|
||||
return cachedIcon;
|
||||
}
|
||||
|
||||
final result = await ref.read(
|
||||
pageInfoProvider(url, isImageRequest: true).future,
|
||||
);
|
||||
|
||||
if (result.favicon case final BrowserIcon favicon) {
|
||||
//If it was a `isImageRequest` hit, we need to cache it at this point
|
||||
if (!_browserIconCache.contains(url.origin)) {
|
||||
_browserIconCache.set(url.origin, favicon);
|
||||
}
|
||||
|
||||
return favicon;
|
||||
}
|
||||
}
|
||||
|
||||
final result = await ref.read(pageInfoProvider(url).future);
|
||||
final favicon = result.favicon!;
|
||||
|
||||
return favicon;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Future<Uri?> tryUpgradeToHttps(Uri httpUri) async {
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'generic_website.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$genericWebsiteServiceHash() =>
|
||||
r'9d17f41596579c289a6cd021ed8d8049e1fb7aff';
|
||||
r'9bba5a6fc01aa788b2674db6ae3869065d6a528d';
|
||||
|
||||
/// See also [GenericWebsiteService].
|
||||
@ProviderFor(GenericWebsiteService)
|
||||
|
||||
@@ -71,7 +71,7 @@ class Bang with FastEquatable implements Insertable<Bang> {
|
||||
: input;
|
||||
}
|
||||
|
||||
Uri getUrl(String? query) {
|
||||
Uri getTemplateUrl(String? query) {
|
||||
final url =
|
||||
(query != null)
|
||||
? urlTemplate.replaceAll(
|
||||
|
||||
@@ -22,7 +22,7 @@ class BangSearch extends _$BangSearch {
|
||||
maxEntryCount: 3,
|
||||
); //TODO: make count dynamic
|
||||
|
||||
return bang.getUrl(searchQuery);
|
||||
return bang.getTemplateUrl(searchQuery);
|
||||
}
|
||||
|
||||
Future<void> search(String input) async {
|
||||
@@ -50,7 +50,7 @@ class BangSearch extends _$BangSearch {
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
class SeamlessBangProvider extends _$SeamlessBangProvider {
|
||||
class SeamlessBang extends _$SeamlessBang {
|
||||
bool _hasSearch = false;
|
||||
|
||||
void search(String input) {
|
||||
@@ -60,6 +60,7 @@ class SeamlessBangProvider extends _$SeamlessBangProvider {
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
//Don't block
|
||||
unawaited(ref.read(bangSearchProvider.notifier).search(input));
|
||||
} else if (_hasSearch) {
|
||||
_hasSearch = false;
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'search.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$bangSearchHash() => r'ff7035d269041c6415f2a2afb0a71ef67d3b9841';
|
||||
String _$bangSearchHash() => r'6b7452d48698c01870c5c75f0c10cadaafc481c8';
|
||||
|
||||
/// See also [BangSearch].
|
||||
@ProviderFor(BangSearch)
|
||||
@@ -23,26 +23,22 @@ final bangSearchProvider =
|
||||
);
|
||||
|
||||
typedef _$BangSearch = AutoDisposeStreamNotifier<List<BangData>>;
|
||||
String _$seamlessBangProviderHash() =>
|
||||
r'c4c0b452c94ba447be5b02dfef5bc16148cc3dc7';
|
||||
String _$seamlessBangHash() => r'8bd7a2cbe4c302ae08f85167290666a7437f8b9b';
|
||||
|
||||
/// See also [SeamlessBangProvider].
|
||||
@ProviderFor(SeamlessBangProvider)
|
||||
final seamlessBangProviderProvider = AutoDisposeNotifierProvider<
|
||||
SeamlessBangProvider,
|
||||
/// See also [SeamlessBang].
|
||||
@ProviderFor(SeamlessBang)
|
||||
final seamlessBangProvider = AutoDisposeNotifierProvider<
|
||||
SeamlessBang,
|
||||
AsyncValue<List<BangData>>
|
||||
>.internal(
|
||||
SeamlessBangProvider.new,
|
||||
name: r'seamlessBangProviderProvider',
|
||||
SeamlessBang.new,
|
||||
name: r'seamlessBangProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$seamlessBangProviderHash,
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$seamlessBangHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$SeamlessBangProvider =
|
||||
AutoDisposeNotifier<AsyncValue<List<BangData>>>;
|
||||
typedef _$SeamlessBang = AutoDisposeNotifier<AsyncValue<List<BangData>>>;
|
||||
// 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,7 +1,6 @@
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
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/bangs/domain/providers/bangs.dart';
|
||||
@@ -19,13 +18,14 @@ class BangCategoriesScreen extends HookConsumerWidget {
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context.push(const BangSearchRoute().location);
|
||||
await const BangSearchRoute().push(context);
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: categoriesAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (categories) {
|
||||
return FadingScroll(
|
||||
fadingSize: 25,
|
||||
@@ -66,13 +66,10 @@ class BangCategoriesScreen extends HookConsumerWidget {
|
||||
(subCategory) => ListTile(
|
||||
title: Text(subCategory),
|
||||
onTap: () async {
|
||||
await context.push(
|
||||
BangSubCategoryRoute(
|
||||
category: category.key,
|
||||
subCategory:
|
||||
subCategory,
|
||||
).location,
|
||||
);
|
||||
await BangSubCategoryRoute(
|
||||
category: category.key,
|
||||
subCategory: subCategory,
|
||||
).push(context);
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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';
|
||||
@@ -29,6 +28,7 @@ class BangListScreen extends HookConsumerWidget {
|
||||
slivers: [
|
||||
SliverAppBar.medium(title: Text('$category: $subCategory')),
|
||||
bangsAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (bangs) {
|
||||
return SliverList.builder(
|
||||
itemCount: bangs.length,
|
||||
@@ -41,7 +41,7 @@ class BangListScreen extends HookConsumerWidget {
|
||||
.read(selectedBangTriggerProvider().notifier)
|
||||
.setTrigger(bang.trigger);
|
||||
|
||||
context.go(const SearchRoute().location);
|
||||
const SearchRoute().go(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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/features/bangs/data/models/bang_data.dart';
|
||||
@@ -38,7 +37,7 @@ class BangDetails extends HookConsumerWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
UrlIcon(bangData.getUrl(''), iconSize: 34.0),
|
||||
UrlIcon([bangData.getTemplateUrl('')], iconSize: 34.0),
|
||||
const SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -67,14 +66,14 @@ class BangDetails extends HookConsumerWidget {
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: () async {
|
||||
final url = Uri.parse(bangData.getUrl('').origin);
|
||||
final url = Uri.parse(bangData.getTemplateUrl('').origin);
|
||||
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(url: url);
|
||||
|
||||
if (context.mounted) {
|
||||
context.go(BrowserRoute().location);
|
||||
BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
label: Text(bangData.domain),
|
||||
|
||||
@@ -73,7 +73,8 @@ class SiteSearch extends HookConsumerWidget {
|
||||
width: double.maxFinite,
|
||||
child: SelectableChips(
|
||||
itemId: (bang) => bang.trigger,
|
||||
itemAvatar: (bang) => UrlIcon(bang.getUrl(''), iconSize: 20),
|
||||
itemAvatar:
|
||||
(bang) => UrlIcon([bang.getTemplateUrl('')], iconSize: 20),
|
||||
itemLabel: (bang) => Text(bang.websiteName),
|
||||
availableItems: availableBangs,
|
||||
selectedItem: selectedBang,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ class AddressWithSuggestionsField extends HookConsumerWidget {
|
||||
defaultSearchBangDataProvider.future,
|
||||
);
|
||||
|
||||
newUrl = defaultSearchBang?.getUrl(value);
|
||||
newUrl = defaultSearchBang?.getTemplateUrl(value);
|
||||
}
|
||||
|
||||
if (newUrl != null) {
|
||||
|
||||
+16
-37
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
+9
@@ -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])),
|
||||
|
||||
+5
-8
@@ -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'),
|
||||
|
||||
+1
-2
@@ -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'),
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+5
-7
@@ -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
-2
@@ -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),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
|
||||
@@ -31,7 +30,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
leading: const Icon(Icons.settings),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
await context.push(GeneralSettingsRoute().location);
|
||||
await GeneralSettingsRoute().push(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -48,7 +47,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
leading: const Icon(MdiIcons.web),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
await context.push(WebEngineSettingsRoute().location);
|
||||
await WebEngineSettingsRoute().push(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -65,7 +64,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
leading: const Icon(MdiIcons.exclamationThick),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
await context.push(BangSettingsRoute().location);
|
||||
await BangSettingsRoute().push(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
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/extensions/nullable.dart';
|
||||
@@ -33,6 +32,7 @@ class WebEngineHardeningScreen extends HookConsumerWidget {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Web Engine Hardening')),
|
||||
body: preferenceGroups.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (data) {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -85,11 +85,9 @@ class WebEngineHardeningScreen extends HookConsumerWidget {
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
await context.push(
|
||||
WebEngineHardeningGroupRoute(
|
||||
group: group.key,
|
||||
).location,
|
||||
);
|
||||
await WebEngineHardeningGroupRoute(
|
||||
group: group.key,
|
||||
).push(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.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';
|
||||
@@ -307,7 +306,7 @@ class WebEngineSettingsScreen extends HookConsumerWidget {
|
||||
leading: const Icon(MdiIcons.shieldLock),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
await context.push(WebEngineHardeningRoute().location);
|
||||
await WebEngineHardeningRoute().push(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -10,7 +10,7 @@ class ArticleDao extends DatabaseAccessor<FeedDatabase> with _$ArticleDaoMixin {
|
||||
ArticleDao(super.attachedDatabase);
|
||||
|
||||
Selectable<FeedArticle> getFeedArticles(Uri? url) {
|
||||
final select = db.article.select();
|
||||
final select = db.articleView.select();
|
||||
|
||||
if (url != null) {
|
||||
select.where((article) => article.feedId.equalsValue(url));
|
||||
@@ -25,7 +25,7 @@ class ArticleDao extends DatabaseAccessor<FeedDatabase> with _$ArticleDaoMixin {
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<FeedArticle> getArticleById(String articleId) {
|
||||
return db.article.select()..where((row) => row.id.equals(articleId));
|
||||
return db.articleView.select()..where((row) => row.id.equals(articleId));
|
||||
}
|
||||
|
||||
Future<void> upsertArticles(List<FeedArticle> articles) {
|
||||
@@ -111,8 +111,6 @@ class ArticleDao extends DatabaseAccessor<FeedDatabase> with _$ArticleDaoMixin {
|
||||
return db.queryArticlesBasic(
|
||||
feedId: feedId?.toString(),
|
||||
query: db.buildLikeQuery(searchString),
|
||||
beforeMatch: matchPrefix,
|
||||
afterMatch: matchSuffix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,19 @@ class FeedDao extends DatabaseAccessor<FeedDatabase> with _$FeedDaoMixin {
|
||||
return db.feed.select();
|
||||
}
|
||||
|
||||
Future<int> updateFeedFetched(Uri url, DateTime fetched) {
|
||||
SingleOrNullSelectable<FeedData> getFeed(Uri feedId) {
|
||||
return db.feed.select()..where((feed) => feed.url.equalsValue(feedId));
|
||||
}
|
||||
|
||||
Future<int> updateFeedFetched(Uri feedId, DateTime fetched) {
|
||||
final statement =
|
||||
db.feed.update()..where((feed) => feed.url.equalsValue(url));
|
||||
db.feed.update()..where((feed) => feed.url.equalsValue(feedId));
|
||||
|
||||
return statement.write(FeedCompanion(lastFetched: Value(fetched)));
|
||||
}
|
||||
|
||||
Future<int> deleteFeed(Uri url) {
|
||||
return db.feed.deleteWhere((feed) => feed.url.equals(url.toString()));
|
||||
Future<int> deleteFeed(Uri feedId) {
|
||||
return db.feed.deleteWhere((feed) => feed.url.equals(feedId.toString()));
|
||||
}
|
||||
|
||||
Future<int> upsertFeed(FeedData feedData) {
|
||||
|
||||
@@ -9,6 +9,8 @@ CREATE TABLE feed (
|
||||
url TEXT PRIMARY KEY NOT NULL MAPPED BY `const UriConverter()`,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
icon TEXT MAPPED BY `const UriConverter()`,
|
||||
site_link TEXT MAPPED BY `const UriConverter()`,
|
||||
authors TEXT MAPPED BY `const FeedAuthorsConverter()`,
|
||||
tags TEXT MAPPED BY `const FeedCategoriesConverter()`,
|
||||
last_fetched DATETIME
|
||||
@@ -31,6 +33,15 @@ CREATE TABLE article (
|
||||
contentPlain TEXT
|
||||
) WITH FeedArticle;
|
||||
|
||||
CREATE VIEW article_view WITH FeedArticle AS
|
||||
SELECT
|
||||
a.*,
|
||||
f.icon
|
||||
FROM
|
||||
article a
|
||||
INNER JOIN
|
||||
feed f on f.url = a.feed_id;
|
||||
|
||||
CREATE INDEX article_feed_id ON article (feed_id);
|
||||
|
||||
CREATE VIRTUAL TABLE article_fts
|
||||
@@ -73,17 +84,19 @@ queryArticlesBasic(:feed_id AS TEXT OR NULL) WITH FeedArticleQueryResult:
|
||||
)
|
||||
SELECT
|
||||
a.*,
|
||||
highlight(article_fts, 0, :beforeMatch, :afterMatch) AS title,
|
||||
f.icon,
|
||||
(
|
||||
bm25(article_fts, weights.title_weight)
|
||||
) AS weighted_rank
|
||||
FROM article_fts(:query) fts
|
||||
FROM article_fts fts
|
||||
INNER JOIN
|
||||
article a ON a.rowid = fts.rowid
|
||||
INNER JOIN
|
||||
feed f ON f.url = a.feed_id
|
||||
CROSS JOIN weights
|
||||
WHERE
|
||||
fts.title LIKE :query AND
|
||||
:feed_id IS NULL OR a.feed_id = :feed_id
|
||||
(:feed_id IS NULL OR a.feed_id = :feed_id)
|
||||
ORDER BY
|
||||
weighted_rank ASC,
|
||||
a.created DESC NULLS LAST;
|
||||
@@ -92,15 +105,16 @@ queryArticlesFullContent(:feed_id AS TEXT OR NULL) WITH FeedArticleQueryResult:
|
||||
WITH weights AS (
|
||||
SELECT
|
||||
-- Customize these weights (higher = more important)
|
||||
5.0 as title_weight, -- Title matches are most important
|
||||
2.0 as summary_weight, -- Summary matches are quite important
|
||||
10.0 as title_weight, -- Title matches are most important
|
||||
3.0 as summary_weight, -- Summary matches are quite important
|
||||
1.0 as content_weight -- Content matches are basic
|
||||
)
|
||||
SELECT
|
||||
a.*,
|
||||
highlight(article_fts, 0, :beforeMatch, :afterMatch) AS title,
|
||||
snippet(article_fts, 1, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS summary,
|
||||
snippet(article_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS content,
|
||||
f.icon,
|
||||
highlight(article_fts, 0, :beforeMatch, :afterMatch) AS title_highlight,
|
||||
snippet(article_fts, 1, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS summary_snippet,
|
||||
snippet(article_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS content_snippet,
|
||||
(
|
||||
bm25(article_fts, weights.title_weight, weights.summary_weight,
|
||||
weights.content_weight)
|
||||
@@ -108,6 +122,8 @@ queryArticlesFullContent(:feed_id AS TEXT OR NULL) WITH FeedArticleQueryResult:
|
||||
FROM article_fts(:query) fts
|
||||
INNER JOIN
|
||||
article a ON a.rowid = fts.rowid
|
||||
INNER JOIN
|
||||
feed f ON f.url = a.feed_id
|
||||
CROSS JOIN weights
|
||||
WHERE
|
||||
:feed_id IS NULL OR a.feed_id = :feed_id
|
||||
|
||||
@@ -33,6 +33,24 @@ class Feed extends Table with TableInfo<Feed, FeedData> {
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
);
|
||||
late final GeneratedColumnWithTypeConverter<Uri?, String> icon =
|
||||
GeneratedColumn<String>(
|
||||
'icon',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
).withConverter<Uri?>(Feed.$convertericonn);
|
||||
late final GeneratedColumnWithTypeConverter<Uri?, String> siteLink =
|
||||
GeneratedColumn<String>(
|
||||
'site_link',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
).withConverter<Uri?>(Feed.$convertersiteLinkn);
|
||||
late final GeneratedColumnWithTypeConverter<List<FeedAuthor>?, String>
|
||||
authors = GeneratedColumn<String>(
|
||||
'authors',
|
||||
@@ -64,6 +82,8 @@ class Feed extends Table with TableInfo<Feed, FeedData> {
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
siteLink,
|
||||
authors,
|
||||
tags,
|
||||
lastFetched,
|
||||
@@ -93,6 +113,18 @@ class Feed extends Table with TableInfo<Feed, FeedData> {
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}description'],
|
||||
),
|
||||
icon: Feed.$convertericonn.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}icon'],
|
||||
),
|
||||
),
|
||||
siteLink: Feed.$convertersiteLinkn.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}site_link'],
|
||||
),
|
||||
),
|
||||
authors: Feed.$converterauthorsn.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
@@ -118,6 +150,12 @@ class Feed extends Table with TableInfo<Feed, FeedData> {
|
||||
}
|
||||
|
||||
static TypeConverter<Uri, String> $converterurl = const UriConverter();
|
||||
static TypeConverter<Uri, String> $convertericon = const UriConverter();
|
||||
static TypeConverter<Uri?, String?> $convertericonn =
|
||||
NullAwareTypeConverter.wrap($convertericon);
|
||||
static TypeConverter<Uri, String> $convertersiteLink = const UriConverter();
|
||||
static TypeConverter<Uri?, String?> $convertersiteLinkn =
|
||||
NullAwareTypeConverter.wrap($convertersiteLink);
|
||||
static TypeConverter<List<FeedAuthor>, String> $converterauthors =
|
||||
const FeedAuthorsConverter();
|
||||
static TypeConverter<List<FeedAuthor>?, String?> $converterauthorsn =
|
||||
@@ -134,6 +172,8 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
final Uri url;
|
||||
final String? title;
|
||||
final String? description;
|
||||
final Uri? icon;
|
||||
final Uri? siteLink;
|
||||
final List<FeedAuthor>? authors;
|
||||
final List<FeedCategory>? tags;
|
||||
final DateTime? lastFetched;
|
||||
@@ -141,6 +181,8 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
required this.url,
|
||||
this.title,
|
||||
this.description,
|
||||
this.icon,
|
||||
this.siteLink,
|
||||
this.authors,
|
||||
this.tags,
|
||||
this.lastFetched,
|
||||
@@ -157,6 +199,14 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
if (!nullToAbsent || description != null) {
|
||||
map['description'] = Variable<String>(description);
|
||||
}
|
||||
if (!nullToAbsent || icon != null) {
|
||||
map['icon'] = Variable<String>(Feed.$convertericonn.toSql(icon));
|
||||
}
|
||||
if (!nullToAbsent || siteLink != null) {
|
||||
map['site_link'] = Variable<String>(
|
||||
Feed.$convertersiteLinkn.toSql(siteLink),
|
||||
);
|
||||
}
|
||||
if (!nullToAbsent || authors != null) {
|
||||
map['authors'] = Variable<String>(Feed.$converterauthorsn.toSql(authors));
|
||||
}
|
||||
@@ -178,6 +228,8 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
url: serializer.fromJson<Uri>(json['url']),
|
||||
title: serializer.fromJson<String?>(json['title']),
|
||||
description: serializer.fromJson<String?>(json['description']),
|
||||
icon: serializer.fromJson<Uri?>(json['icon']),
|
||||
siteLink: serializer.fromJson<Uri?>(json['site_link']),
|
||||
authors: serializer.fromJson<List<FeedAuthor>?>(json['authors']),
|
||||
tags: serializer.fromJson<List<FeedCategory>?>(json['tags']),
|
||||
lastFetched: serializer.fromJson<DateTime?>(json['last_fetched']),
|
||||
@@ -190,6 +242,8 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
'url': serializer.toJson<Uri>(url),
|
||||
'title': serializer.toJson<String?>(title),
|
||||
'description': serializer.toJson<String?>(description),
|
||||
'icon': serializer.toJson<Uri?>(icon),
|
||||
'site_link': serializer.toJson<Uri?>(siteLink),
|
||||
'authors': serializer.toJson<List<FeedAuthor>?>(authors),
|
||||
'tags': serializer.toJson<List<FeedCategory>?>(tags),
|
||||
'last_fetched': serializer.toJson<DateTime?>(lastFetched),
|
||||
@@ -200,6 +254,8 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
Uri? url,
|
||||
Value<String?> title = const Value.absent(),
|
||||
Value<String?> description = const Value.absent(),
|
||||
Value<Uri?> icon = const Value.absent(),
|
||||
Value<Uri?> siteLink = const Value.absent(),
|
||||
Value<List<FeedAuthor>?> authors = const Value.absent(),
|
||||
Value<List<FeedCategory>?> tags = const Value.absent(),
|
||||
Value<DateTime?> lastFetched = const Value.absent(),
|
||||
@@ -207,6 +263,8 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
url: url ?? this.url,
|
||||
title: title.present ? title.value : this.title,
|
||||
description: description.present ? description.value : this.description,
|
||||
icon: icon.present ? icon.value : this.icon,
|
||||
siteLink: siteLink.present ? siteLink.value : this.siteLink,
|
||||
authors: authors.present ? authors.value : this.authors,
|
||||
tags: tags.present ? tags.value : this.tags,
|
||||
lastFetched: lastFetched.present ? lastFetched.value : this.lastFetched,
|
||||
@@ -217,6 +275,8 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
title: data.title.present ? data.title.value : this.title,
|
||||
description:
|
||||
data.description.present ? data.description.value : this.description,
|
||||
icon: data.icon.present ? data.icon.value : this.icon,
|
||||
siteLink: data.siteLink.present ? data.siteLink.value : this.siteLink,
|
||||
authors: data.authors.present ? data.authors.value : this.authors,
|
||||
tags: data.tags.present ? data.tags.value : this.tags,
|
||||
lastFetched:
|
||||
@@ -230,6 +290,8 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
..write('url: $url, ')
|
||||
..write('title: $title, ')
|
||||
..write('description: $description, ')
|
||||
..write('icon: $icon, ')
|
||||
..write('siteLink: $siteLink, ')
|
||||
..write('authors: $authors, ')
|
||||
..write('tags: $tags, ')
|
||||
..write('lastFetched: $lastFetched')
|
||||
@@ -238,8 +300,16 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(url, title, description, authors, tags, lastFetched);
|
||||
int get hashCode => Object.hash(
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
siteLink,
|
||||
authors,
|
||||
tags,
|
||||
lastFetched,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
@@ -247,6 +317,8 @@ class FeedData extends DataClass implements Insertable<FeedData> {
|
||||
other.url == this.url &&
|
||||
other.title == this.title &&
|
||||
other.description == this.description &&
|
||||
other.icon == this.icon &&
|
||||
other.siteLink == this.siteLink &&
|
||||
other.authors == this.authors &&
|
||||
other.tags == this.tags &&
|
||||
other.lastFetched == this.lastFetched);
|
||||
@@ -256,6 +328,8 @@ class FeedCompanion extends UpdateCompanion<FeedData> {
|
||||
final Value<Uri> url;
|
||||
final Value<String?> title;
|
||||
final Value<String?> description;
|
||||
final Value<Uri?> icon;
|
||||
final Value<Uri?> siteLink;
|
||||
final Value<List<FeedAuthor>?> authors;
|
||||
final Value<List<FeedCategory>?> tags;
|
||||
final Value<DateTime?> lastFetched;
|
||||
@@ -264,6 +338,8 @@ class FeedCompanion extends UpdateCompanion<FeedData> {
|
||||
this.url = const Value.absent(),
|
||||
this.title = const Value.absent(),
|
||||
this.description = const Value.absent(),
|
||||
this.icon = const Value.absent(),
|
||||
this.siteLink = const Value.absent(),
|
||||
this.authors = const Value.absent(),
|
||||
this.tags = const Value.absent(),
|
||||
this.lastFetched = const Value.absent(),
|
||||
@@ -273,6 +349,8 @@ class FeedCompanion extends UpdateCompanion<FeedData> {
|
||||
required Uri url,
|
||||
this.title = const Value.absent(),
|
||||
this.description = const Value.absent(),
|
||||
this.icon = const Value.absent(),
|
||||
this.siteLink = const Value.absent(),
|
||||
this.authors = const Value.absent(),
|
||||
this.tags = const Value.absent(),
|
||||
this.lastFetched = const Value.absent(),
|
||||
@@ -282,6 +360,8 @@ class FeedCompanion extends UpdateCompanion<FeedData> {
|
||||
Expression<String>? url,
|
||||
Expression<String>? title,
|
||||
Expression<String>? description,
|
||||
Expression<String>? icon,
|
||||
Expression<String>? siteLink,
|
||||
Expression<String>? authors,
|
||||
Expression<String>? tags,
|
||||
Expression<DateTime>? lastFetched,
|
||||
@@ -291,6 +371,8 @@ class FeedCompanion extends UpdateCompanion<FeedData> {
|
||||
if (url != null) 'url': url,
|
||||
if (title != null) 'title': title,
|
||||
if (description != null) 'description': description,
|
||||
if (icon != null) 'icon': icon,
|
||||
if (siteLink != null) 'site_link': siteLink,
|
||||
if (authors != null) 'authors': authors,
|
||||
if (tags != null) 'tags': tags,
|
||||
if (lastFetched != null) 'last_fetched': lastFetched,
|
||||
@@ -302,6 +384,8 @@ class FeedCompanion extends UpdateCompanion<FeedData> {
|
||||
Value<Uri>? url,
|
||||
Value<String?>? title,
|
||||
Value<String?>? description,
|
||||
Value<Uri?>? icon,
|
||||
Value<Uri?>? siteLink,
|
||||
Value<List<FeedAuthor>?>? authors,
|
||||
Value<List<FeedCategory>?>? tags,
|
||||
Value<DateTime?>? lastFetched,
|
||||
@@ -311,6 +395,8 @@ class FeedCompanion extends UpdateCompanion<FeedData> {
|
||||
url: url ?? this.url,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
icon: icon ?? this.icon,
|
||||
siteLink: siteLink ?? this.siteLink,
|
||||
authors: authors ?? this.authors,
|
||||
tags: tags ?? this.tags,
|
||||
lastFetched: lastFetched ?? this.lastFetched,
|
||||
@@ -330,6 +416,14 @@ class FeedCompanion extends UpdateCompanion<FeedData> {
|
||||
if (description.present) {
|
||||
map['description'] = Variable<String>(description.value);
|
||||
}
|
||||
if (icon.present) {
|
||||
map['icon'] = Variable<String>(Feed.$convertericonn.toSql(icon.value));
|
||||
}
|
||||
if (siteLink.present) {
|
||||
map['site_link'] = Variable<String>(
|
||||
Feed.$convertersiteLinkn.toSql(siteLink.value),
|
||||
);
|
||||
}
|
||||
if (authors.present) {
|
||||
map['authors'] = Variable<String>(
|
||||
Feed.$converterauthorsn.toSql(authors.value),
|
||||
@@ -353,6 +447,8 @@ class FeedCompanion extends UpdateCompanion<FeedData> {
|
||||
..write('url: $url, ')
|
||||
..write('title: $title, ')
|
||||
..write('description: $description, ')
|
||||
..write('icon: $icon, ')
|
||||
..write('siteLink: $siteLink, ')
|
||||
..write('authors: $authors, ')
|
||||
..write('tags: $tags, ')
|
||||
..write('lastFetched: $lastFetched, ')
|
||||
@@ -806,6 +902,226 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
|
||||
}
|
||||
}
|
||||
|
||||
class ArticleView extends ViewInfo<ArticleView, FeedArticle>
|
||||
implements HasResultSet {
|
||||
final String? _alias;
|
||||
@override
|
||||
final _$FeedDatabase attachedDatabase;
|
||||
ArticleView(this.attachedDatabase, [this._alias]);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
feedId,
|
||||
fetched,
|
||||
created,
|
||||
updated,
|
||||
lastRead,
|
||||
title,
|
||||
authors,
|
||||
tags,
|
||||
links,
|
||||
summaryMarkdown,
|
||||
summaryPlain,
|
||||
contentMarkdown,
|
||||
contentPlain,
|
||||
icon,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? entityName;
|
||||
@override
|
||||
String get entityName => 'article_view';
|
||||
@override
|
||||
Map<SqlDialect, String> get createViewStatements => {
|
||||
SqlDialect.sqlite:
|
||||
'CREATE VIEW article_view AS SELECT a.*, f.icon FROM article AS a INNER JOIN feed AS f ON f.url = a.feed_id',
|
||||
};
|
||||
@override
|
||||
ArticleView get asDslTable => this;
|
||||
@override
|
||||
FeedArticle map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return FeedArticle(
|
||||
id:
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}id'],
|
||||
)!,
|
||||
feedId: Article.$converterfeedId.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}feed_id'],
|
||||
)!,
|
||||
),
|
||||
fetched:
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}fetched'],
|
||||
)!,
|
||||
created: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}created'],
|
||||
),
|
||||
updated: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}updated'],
|
||||
),
|
||||
lastRead: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}last_read'],
|
||||
),
|
||||
title: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}title'],
|
||||
),
|
||||
authors: Article.$converterauthorsn.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}authors'],
|
||||
),
|
||||
),
|
||||
tags: Article.$convertertagsn.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}tags'],
|
||||
),
|
||||
),
|
||||
links: Article.$converterlinksn.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}links'],
|
||||
),
|
||||
),
|
||||
summaryMarkdown: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}summaryMarkdown'],
|
||||
),
|
||||
summaryPlain: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}summaryPlain'],
|
||||
),
|
||||
contentMarkdown: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}contentMarkdown'],
|
||||
),
|
||||
contentPlain: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}contentPlain'],
|
||||
),
|
||||
icon: Feed.$convertericonn.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}icon'],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
late final GeneratedColumn<String> id = GeneratedColumn<String>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
);
|
||||
late final GeneratedColumnWithTypeConverter<Uri, String> feedId =
|
||||
GeneratedColumn<String>(
|
||||
'feed_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
).withConverter<Uri>(Article.$converterfeedId);
|
||||
late final GeneratedColumn<DateTime> fetched = GeneratedColumn<DateTime>(
|
||||
'fetched',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
);
|
||||
late final GeneratedColumn<DateTime> created = GeneratedColumn<DateTime>(
|
||||
'created',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.dateTime,
|
||||
);
|
||||
late final GeneratedColumn<DateTime> updated = GeneratedColumn<DateTime>(
|
||||
'updated',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.dateTime,
|
||||
);
|
||||
late final GeneratedColumn<DateTime> lastRead = GeneratedColumn<DateTime>(
|
||||
'last_read',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.dateTime,
|
||||
);
|
||||
late final GeneratedColumn<String> title = GeneratedColumn<String>(
|
||||
'title',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
);
|
||||
late final GeneratedColumnWithTypeConverter<List<FeedAuthor>?, String>
|
||||
authors = GeneratedColumn<String>(
|
||||
'authors',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
).withConverter<List<FeedAuthor>?>(Article.$converterauthorsn);
|
||||
late final GeneratedColumnWithTypeConverter<List<FeedCategory>?, String>
|
||||
tags = GeneratedColumn<String>(
|
||||
'tags',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
).withConverter<List<FeedCategory>?>(Article.$convertertagsn);
|
||||
late final GeneratedColumnWithTypeConverter<List<FeedLink>?, String> links =
|
||||
GeneratedColumn<String>(
|
||||
'links',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
).withConverter<List<FeedLink>?>(Article.$converterlinksn);
|
||||
late final GeneratedColumn<String> summaryMarkdown = GeneratedColumn<String>(
|
||||
'summaryMarkdown',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
);
|
||||
late final GeneratedColumn<String> summaryPlain = GeneratedColumn<String>(
|
||||
'summaryPlain',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
);
|
||||
late final GeneratedColumn<String> contentMarkdown = GeneratedColumn<String>(
|
||||
'contentMarkdown',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
);
|
||||
late final GeneratedColumn<String> contentPlain = GeneratedColumn<String>(
|
||||
'contentPlain',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
);
|
||||
late final GeneratedColumnWithTypeConverter<Uri?, String> icon =
|
||||
GeneratedColumn<String>(
|
||||
'icon',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
).withConverter<Uri?>(Feed.$convertericonn);
|
||||
@override
|
||||
ArticleView createAlias(String alias) {
|
||||
return ArticleView(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
Query? get query => null;
|
||||
@override
|
||||
Set<String> get readTables => const {'article', 'feed'};
|
||||
}
|
||||
|
||||
class ArticleFts extends Table
|
||||
with
|
||||
TableInfo<ArticleFts, ArticleFt>,
|
||||
@@ -1046,6 +1362,7 @@ abstract class _$FeedDatabase extends GeneratedDatabase {
|
||||
$FeedDatabaseManager get managers => $FeedDatabaseManager(this);
|
||||
late final Feed feed = Feed(this);
|
||||
late final Article article = Article(this);
|
||||
late final ArticleView articleView = ArticleView(this);
|
||||
late final Index articleFeedId = Index(
|
||||
'article_feed_id',
|
||||
'CREATE INDEX article_feed_id ON article (feed_id)',
|
||||
@@ -1074,20 +1391,13 @@ abstract class _$FeedDatabase extends GeneratedDatabase {
|
||||
}
|
||||
|
||||
Selectable<FeedArticleQueryResult> queryArticlesBasic({
|
||||
required String beforeMatch,
|
||||
required String afterMatch,
|
||||
required String query,
|
||||
String? feedId,
|
||||
}) {
|
||||
return customSelect(
|
||||
'WITH weights AS (SELECT 1.0 AS title_weight) SELECT a.*, highlight(article_fts, 0, ?1, ?2) AS title,(bm25(article_fts, weights.title_weight))AS weighted_rank FROM article_fts(?3)AS fts INNER JOIN article AS a ON a."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?3 AND ?4 IS NULL OR a.feed_id = ?4 ORDER BY weighted_rank ASC, a.created DESC NULLS LAST',
|
||||
variables: [
|
||||
Variable<String>(beforeMatch),
|
||||
Variable<String>(afterMatch),
|
||||
Variable<String>(query),
|
||||
Variable<String>(feedId),
|
||||
],
|
||||
readsFrom: {articleFts, article},
|
||||
'WITH weights AS (SELECT 1.0 AS title_weight) SELECT a.*, f.icon,(bm25(article_fts, weights.title_weight))AS weighted_rank FROM article_fts AS fts INNER JOIN article AS a ON a."rowid" = fts."rowid" INNER JOIN feed AS f ON f.url = a.feed_id CROSS JOIN weights WHERE fts.title LIKE ?1 AND(?2 IS NULL OR a.feed_id = ?2)ORDER BY weighted_rank ASC, a.created DESC NULLS LAST',
|
||||
variables: [Variable<String>(query), Variable<String>(feedId)],
|
||||
readsFrom: {feed, articleFts, article},
|
||||
).map(
|
||||
(QueryRow row) => FeedArticleQueryResult(
|
||||
id: row.read<String>('id'),
|
||||
@@ -1114,6 +1424,10 @@ abstract class _$FeedDatabase extends GeneratedDatabase {
|
||||
summaryPlain: row.readNullable<String>('summaryPlain'),
|
||||
contentMarkdown: row.readNullable<String>('contentMarkdown'),
|
||||
contentPlain: row.readNullable<String>('contentPlain'),
|
||||
icon: NullAwareTypeConverter.wrapFromSql(
|
||||
Feed.$convertericon,
|
||||
row.readNullable<String>('icon'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1127,7 +1441,7 @@ abstract class _$FeedDatabase extends GeneratedDatabase {
|
||||
String? feedId,
|
||||
}) {
|
||||
return customSelect(
|
||||
'WITH weights AS (SELECT 5.0 AS title_weight, 2.0 AS summary_weight, 1.0 AS content_weight) SELECT a.*, highlight(article_fts, 0, ?1, ?2) AS title, snippet(article_fts, 1, ?1, ?2, ?3, ?4) AS summary, snippet(article_fts, 2, ?1, ?2, ?3, ?4) AS content,(bm25(article_fts, weights.title_weight, weights.summary_weight, weights.content_weight))AS weighted_rank FROM article_fts(?5)AS fts INNER JOIN article AS a ON a."rowid" = fts."rowid" CROSS JOIN weights WHERE ?6 IS NULL OR a.feed_id = ?6 ORDER BY weighted_rank ASC, a.created DESC NULLS LAST',
|
||||
'WITH weights AS (SELECT 10.0 AS title_weight, 3.0 AS summary_weight, 1.0 AS content_weight) SELECT a.*, f.icon, highlight(article_fts, 0, ?1, ?2) AS title_highlight, snippet(article_fts, 1, ?1, ?2, ?3, ?4) AS summary_snippet, snippet(article_fts, 2, ?1, ?2, ?3, ?4) AS content_snippet,(bm25(article_fts, weights.title_weight, weights.summary_weight, weights.content_weight))AS weighted_rank FROM article_fts(?5)AS fts INNER JOIN article AS a ON a."rowid" = fts."rowid" INNER JOIN feed AS f ON f.url = a.feed_id CROSS JOIN weights WHERE ?6 IS NULL OR a.feed_id = ?6 ORDER BY weighted_rank ASC, a.created DESC NULLS LAST',
|
||||
variables: [
|
||||
Variable<String>(beforeMatch),
|
||||
Variable<String>(afterMatch),
|
||||
@@ -1136,7 +1450,7 @@ abstract class _$FeedDatabase extends GeneratedDatabase {
|
||||
Variable<String>(query),
|
||||
Variable<String>(feedId),
|
||||
],
|
||||
readsFrom: {articleFts, article},
|
||||
readsFrom: {feed, articleFts, article},
|
||||
).map(
|
||||
(QueryRow row) => FeedArticleQueryResult(
|
||||
id: row.read<String>('id'),
|
||||
@@ -1163,6 +1477,13 @@ abstract class _$FeedDatabase extends GeneratedDatabase {
|
||||
summaryPlain: row.readNullable<String>('summaryPlain'),
|
||||
contentMarkdown: row.readNullable<String>('contentMarkdown'),
|
||||
contentPlain: row.readNullable<String>('contentPlain'),
|
||||
icon: NullAwareTypeConverter.wrapFromSql(
|
||||
Feed.$convertericon,
|
||||
row.readNullable<String>('icon'),
|
||||
),
|
||||
titleHighlight: row.readNullable<String>('title_highlight'),
|
||||
summarySnippet: row.readNullable<String>('summary_snippet'),
|
||||
contentSnippet: row.readNullable<String>('content_snippet'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1174,6 +1495,7 @@ abstract class _$FeedDatabase extends GeneratedDatabase {
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [
|
||||
feed,
|
||||
article,
|
||||
articleView,
|
||||
articleFeedId,
|
||||
articleFts,
|
||||
articleAfterInsert,
|
||||
@@ -1218,6 +1540,8 @@ typedef $FeedCreateCompanionBuilder =
|
||||
required Uri url,
|
||||
Value<String?> title,
|
||||
Value<String?> description,
|
||||
Value<Uri?> icon,
|
||||
Value<Uri?> siteLink,
|
||||
Value<List<FeedAuthor>?> authors,
|
||||
Value<List<FeedCategory>?> tags,
|
||||
Value<DateTime?> lastFetched,
|
||||
@@ -1228,6 +1552,8 @@ typedef $FeedUpdateCompanionBuilder =
|
||||
Value<Uri> url,
|
||||
Value<String?> title,
|
||||
Value<String?> description,
|
||||
Value<Uri?> icon,
|
||||
Value<Uri?> siteLink,
|
||||
Value<List<FeedAuthor>?> authors,
|
||||
Value<List<FeedCategory>?> tags,
|
||||
Value<DateTime?> lastFetched,
|
||||
@@ -1282,6 +1608,18 @@ class $FeedFilterComposer extends Composer<_$FeedDatabase, Feed> {
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnWithTypeConverterFilters<Uri?, Uri, String> get icon =>
|
||||
$composableBuilder(
|
||||
column: $table.icon,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
ColumnWithTypeConverterFilters<Uri?, Uri, String> get siteLink =>
|
||||
$composableBuilder(
|
||||
column: $table.siteLink,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
ColumnWithTypeConverterFilters<List<FeedAuthor>?, List<FeedAuthor>, String>
|
||||
get authors => $composableBuilder(
|
||||
column: $table.authors,
|
||||
@@ -1352,6 +1690,16 @@ class $FeedOrderingComposer extends Composer<_$FeedDatabase, Feed> {
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get icon => $composableBuilder(
|
||||
column: $table.icon,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get siteLink => $composableBuilder(
|
||||
column: $table.siteLink,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get authors => $composableBuilder(
|
||||
column: $table.authors,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
@@ -1387,6 +1735,12 @@ class $FeedAnnotationComposer extends Composer<_$FeedDatabase, Feed> {
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumnWithTypeConverter<Uri?, String> get icon =>
|
||||
$composableBuilder(column: $table.icon, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<Uri?, String> get siteLink =>
|
||||
$composableBuilder(column: $table.siteLink, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<List<FeedAuthor>?, String> get authors =>
|
||||
$composableBuilder(column: $table.authors, builder: (column) => column);
|
||||
|
||||
@@ -1455,6 +1809,8 @@ class $FeedTableManager
|
||||
Value<Uri> url = const Value.absent(),
|
||||
Value<String?> title = const Value.absent(),
|
||||
Value<String?> description = const Value.absent(),
|
||||
Value<Uri?> icon = const Value.absent(),
|
||||
Value<Uri?> siteLink = const Value.absent(),
|
||||
Value<List<FeedAuthor>?> authors = const Value.absent(),
|
||||
Value<List<FeedCategory>?> tags = const Value.absent(),
|
||||
Value<DateTime?> lastFetched = const Value.absent(),
|
||||
@@ -1463,6 +1819,8 @@ class $FeedTableManager
|
||||
url: url,
|
||||
title: title,
|
||||
description: description,
|
||||
icon: icon,
|
||||
siteLink: siteLink,
|
||||
authors: authors,
|
||||
tags: tags,
|
||||
lastFetched: lastFetched,
|
||||
@@ -1473,6 +1831,8 @@ class $FeedTableManager
|
||||
required Uri url,
|
||||
Value<String?> title = const Value.absent(),
|
||||
Value<String?> description = const Value.absent(),
|
||||
Value<Uri?> icon = const Value.absent(),
|
||||
Value<Uri?> siteLink = const Value.absent(),
|
||||
Value<List<FeedAuthor>?> authors = const Value.absent(),
|
||||
Value<List<FeedCategory>?> tags = const Value.absent(),
|
||||
Value<DateTime?> lastFetched = const Value.absent(),
|
||||
@@ -1481,6 +1841,8 @@ class $FeedTableManager
|
||||
url: url,
|
||||
title: title,
|
||||
description: description,
|
||||
icon: icon,
|
||||
siteLink: siteLink,
|
||||
authors: authors,
|
||||
tags: tags,
|
||||
lastFetched: lastFetched,
|
||||
|
||||
@@ -25,6 +25,9 @@ class FeedArticle with FastEquatable implements Insertable<FeedArticle> {
|
||||
final String? contentMarkdown;
|
||||
final String? contentPlain;
|
||||
|
||||
//Derived by view from feed table, should not get inserted
|
||||
final Uri? icon;
|
||||
|
||||
FeedArticle({
|
||||
required this.id,
|
||||
required this.feedId,
|
||||
@@ -40,6 +43,7 @@ class FeedArticle with FastEquatable implements Insertable<FeedArticle> {
|
||||
this.summaryPlain,
|
||||
this.contentMarkdown,
|
||||
this.contentPlain,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
factory FeedArticle.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -109,5 +113,6 @@ class FeedArticle with FastEquatable implements Insertable<FeedArticle> {
|
||||
summaryPlain,
|
||||
contentMarkdown,
|
||||
contentPlain,
|
||||
icon,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ FeedArticle _$FeedArticleFromJson(Map<String, dynamic> json) => FeedArticle(
|
||||
summaryPlain: json['summaryPlain'] as String?,
|
||||
contentMarkdown: json['contentMarkdown'] as String?,
|
||||
contentPlain: json['contentPlain'] as String?,
|
||||
icon: json['icon'] == null ? null : Uri.parse(json['icon'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FeedArticleToJson(FeedArticle instance) =>
|
||||
@@ -57,4 +58,5 @@ Map<String, dynamic> _$FeedArticleToJson(FeedArticle instance) =>
|
||||
'summaryPlain': instance.summaryPlain,
|
||||
'contentMarkdown': instance.contentMarkdown,
|
||||
'contentPlain': instance.contentPlain,
|
||||
'icon': instance.icon?.toString(),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'package:lensai/features/web_feed/data/models/feed_article.dart';
|
||||
|
||||
class FeedArticleQueryResult extends FeedArticle {
|
||||
final String? titleHighlight;
|
||||
final String? summarySnippet;
|
||||
final String? contentSnippet;
|
||||
|
||||
final double weightedRank;
|
||||
|
||||
FeedArticleQueryResult({
|
||||
@@ -8,19 +12,29 @@ class FeedArticleQueryResult extends FeedArticle {
|
||||
required super.feedId,
|
||||
required super.fetched,
|
||||
required this.weightedRank,
|
||||
super.created,
|
||||
super.updated,
|
||||
super.lastRead,
|
||||
super.title,
|
||||
super.authors,
|
||||
super.tags,
|
||||
super.links,
|
||||
super.summaryMarkdown,
|
||||
super.summaryPlain,
|
||||
super.contentMarkdown,
|
||||
super.contentPlain,
|
||||
required super.created,
|
||||
required super.updated,
|
||||
required super.lastRead,
|
||||
required super.title,
|
||||
required super.authors,
|
||||
required super.tags,
|
||||
required super.links,
|
||||
required super.summaryMarkdown,
|
||||
required super.summaryPlain,
|
||||
required super.contentMarkdown,
|
||||
required super.contentPlain,
|
||||
required super.icon,
|
||||
this.titleHighlight,
|
||||
this.summarySnippet,
|
||||
this.contentSnippet,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [...super.hashParameters, weightedRank];
|
||||
List<Object?> get hashParameters => [
|
||||
...super.hashParameters,
|
||||
weightedRank,
|
||||
summarySnippet,
|
||||
contentSnippet,
|
||||
titleHighlight,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
part 'feed_filter.g.dart';
|
||||
|
||||
@CopyWith()
|
||||
class FeedFilter with FastEquatable {
|
||||
final Uri? feedId;
|
||||
final String? query;
|
||||
final Set<String>? tags;
|
||||
|
||||
FeedFilter({this.feedId, this.query, this.tags});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [feedId, query, tags];
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'feed_filter.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$FeedFilterCWProxy {
|
||||
FeedFilter feedId(Uri? feedId);
|
||||
|
||||
FeedFilter query(String? query);
|
||||
|
||||
FeedFilter tags(Set<String>? tags);
|
||||
|
||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FeedFilter(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||
///
|
||||
/// Usage
|
||||
/// ```dart
|
||||
/// FeedFilter(...).copyWith(id: 12, name: "My name")
|
||||
/// ````
|
||||
FeedFilter call({Uri? feedId, String? query, Set<String>? tags});
|
||||
}
|
||||
|
||||
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfFeedFilter.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfFeedFilter.copyWith.fieldName(...)`
|
||||
class _$FeedFilterCWProxyImpl implements _$FeedFilterCWProxy {
|
||||
const _$FeedFilterCWProxyImpl(this._value);
|
||||
|
||||
final FeedFilter _value;
|
||||
|
||||
@override
|
||||
FeedFilter feedId(Uri? feedId) => this(feedId: feedId);
|
||||
|
||||
@override
|
||||
FeedFilter query(String? query) => this(query: query);
|
||||
|
||||
@override
|
||||
FeedFilter tags(Set<String>? tags) => this(tags: tags);
|
||||
|
||||
@override
|
||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FeedFilter(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||
///
|
||||
/// Usage
|
||||
/// ```dart
|
||||
/// FeedFilter(...).copyWith(id: 12, name: "My name")
|
||||
/// ````
|
||||
FeedFilter call({
|
||||
Object? feedId = const $CopyWithPlaceholder(),
|
||||
Object? query = const $CopyWithPlaceholder(),
|
||||
Object? tags = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return FeedFilter(
|
||||
feedId:
|
||||
feedId == const $CopyWithPlaceholder()
|
||||
? _value.feedId
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: feedId as Uri?,
|
||||
query:
|
||||
query == const $CopyWithPlaceholder()
|
||||
? _value.query
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: query as String?,
|
||||
tags:
|
||||
tags == const $CopyWithPlaceholder()
|
||||
? _value.tags
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: tags as Set<String>?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $FeedFilterCopyWith on FeedFilter {
|
||||
/// Returns a callable class that can be used as follows: `instanceOfFeedFilter.copyWith(...)` or like so:`instanceOfFeedFilter.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$FeedFilterCWProxy get copyWith => _$FeedFilterCWProxyImpl(this);
|
||||
}
|
||||
@@ -1,12 +1,62 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/web_feed/data/database/database.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_filter.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_article.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_parse_result.dart';
|
||||
import 'package:lensai/features/web_feed/data/providers.dart';
|
||||
import 'package:lensai/features/web_feed/domain/providers/article_filter.dart';
|
||||
import 'package:lensai/features/web_feed/domain/repositories/feed_repository.dart';
|
||||
import 'package:lensai/features/web_feed/domain/services/feed_reader.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
class ArticleSearch extends _$ArticleSearch {
|
||||
late StreamController<List<FeedArticle>> _streamController;
|
||||
|
||||
Future<void> search(
|
||||
String input, {
|
||||
int snippetLength = 120,
|
||||
String matchPrefix = '***',
|
||||
String matchSuffix = '***',
|
||||
String ellipsis = '…',
|
||||
}) async {
|
||||
if (input.isNotEmpty) {
|
||||
await ref
|
||||
.read(feedDatabaseProvider)
|
||||
.articleDao
|
||||
.queryArticles(
|
||||
matchPrefix: matchPrefix,
|
||||
matchSuffix: matchSuffix,
|
||||
ellipsis: ellipsis,
|
||||
snippetLength: snippetLength,
|
||||
searchString: input,
|
||||
feedId: feedId,
|
||||
)
|
||||
.get()
|
||||
.then((value) {
|
||||
if (!_streamController.isClosed) {
|
||||
_streamController.add(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<FeedArticle>> build(Uri? feedId) {
|
||||
_streamController = StreamController();
|
||||
|
||||
ref.onDispose(() async {
|
||||
await _streamController.close();
|
||||
});
|
||||
|
||||
return _streamController.stream;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<FeedData>> feedList(Ref ref) {
|
||||
final repository = ref.watch(feedRepositoryProvider.notifier);
|
||||
@@ -14,25 +64,102 @@ Stream<List<FeedData>> feedList(Ref ref) {
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<FeedArticle>> feedArticleList(Ref ref, FeedFilter filter) {
|
||||
Stream<FeedData?> feedData(Ref ref, Uri? feedId) {
|
||||
final repository = ref.watch(feedRepositoryProvider.notifier);
|
||||
return repository.watchFeedArticles(filter);
|
||||
|
||||
if (feedId == null) {
|
||||
return Stream.value(null);
|
||||
}
|
||||
|
||||
return repository.watchFeed(feedId);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<FeedArticle?> feedArticle(Ref ref, String articleId) {
|
||||
Stream<List<FeedArticle>> feedArticleList(Ref ref, Uri? feedId) {
|
||||
final repository = ref.watch(feedRepositoryProvider.notifier);
|
||||
return repository.watchArticle(articleId);
|
||||
return repository.watchFeedArticles(feedId);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Raw<Stream<Map<String, int>>> _unreadArticleCount(Ref ref) {
|
||||
class FilteredArticleList extends _$FilteredArticleList {
|
||||
bool _hasSearch = false;
|
||||
|
||||
void search(String input) {
|
||||
if (input.isNotEmpty) {
|
||||
if (!_hasSearch) {
|
||||
_hasSearch = true;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
//Don't block
|
||||
unawaited(ref.read(articleSearchProvider(feedId).notifier).search(input));
|
||||
} else if (_hasSearch) {
|
||||
_hasSearch = false;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
AsyncValue<List<FeedArticle>> build(Uri? feedId) {
|
||||
final filterTags = ref.watch(articleFilterProvider);
|
||||
|
||||
final articlesAsync =
|
||||
_hasSearch
|
||||
? ref.watch(articleSearchProvider(feedId))
|
||||
: ref.watch(feedArticleListProvider(feedId));
|
||||
|
||||
return articlesAsync.whenData((articles) {
|
||||
if (filterTags.isNotEmpty) {
|
||||
return articles.where((article) {
|
||||
final tags = article.tags?.map((tag) => tag.id).toSet();
|
||||
|
||||
final authors =
|
||||
article.authors
|
||||
?.map((author) => author.name.whenNotEmpty)
|
||||
.nonNulls
|
||||
.toSet();
|
||||
|
||||
return filterTags.every(
|
||||
(filter) =>
|
||||
(tags?.contains(filter) ?? false) ||
|
||||
(authors?.contains(filter) ?? false),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return articles;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<FeedArticle?> feedArticle(
|
||||
Ref ref,
|
||||
String articleId, {
|
||||
required bool updateReadDate,
|
||||
}) async* {
|
||||
final repository = ref.watch(feedRepositoryProvider.notifier);
|
||||
|
||||
if (updateReadDate) {
|
||||
await repository.touchArticleRead(articleId);
|
||||
}
|
||||
|
||||
yield* repository.watchArticle(articleId);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Raw<Stream<Map<String, int>>> unreadArticleCount(Ref ref) {
|
||||
final repository = ref.watch(feedRepositoryProvider.notifier);
|
||||
return repository.watchUnreadFeedArticleCount();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<int?> unreadFeedArticleCount(Ref ref, Uri feedId) {
|
||||
final stream = ref.watch(_unreadArticleCountProvider);
|
||||
final stream = ref.watch(unreadArticleCountProvider);
|
||||
return stream.map((counts) => counts[feedId.toString()]);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Future<FeedParseResult> fetchWebFeed(Ref ref, Uri url) {
|
||||
return ref.read(feedReaderProvider.notifier).parseFeed(url);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ final feedListProvider = AutoDisposeStreamProvider<List<FeedData>>.internal(
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef FeedListRef = AutoDisposeStreamProviderRef<List<FeedData>>;
|
||||
String _$feedArticleListHash() => r'64e834a1b69d913f1c860b38956b69af9e89a833';
|
||||
String _$feedDataHash() => r'0599a2e3d159ef3abb6c3d2c871f87da2f5e646b';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -45,6 +45,124 @@ class _SystemHash {
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [feedData].
|
||||
@ProviderFor(feedData)
|
||||
const feedDataProvider = FeedDataFamily();
|
||||
|
||||
/// See also [feedData].
|
||||
class FeedDataFamily extends Family<AsyncValue<FeedData?>> {
|
||||
/// See also [feedData].
|
||||
const FeedDataFamily();
|
||||
|
||||
/// See also [feedData].
|
||||
FeedDataProvider call(Uri? feedId) {
|
||||
return FeedDataProvider(feedId);
|
||||
}
|
||||
|
||||
@override
|
||||
FeedDataProvider getProviderOverride(covariant FeedDataProvider provider) {
|
||||
return call(provider.feedId);
|
||||
}
|
||||
|
||||
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'feedDataProvider';
|
||||
}
|
||||
|
||||
/// See also [feedData].
|
||||
class FeedDataProvider extends AutoDisposeStreamProvider<FeedData?> {
|
||||
/// See also [feedData].
|
||||
FeedDataProvider(Uri? feedId)
|
||||
: this._internal(
|
||||
(ref) => feedData(ref as FeedDataRef, feedId),
|
||||
from: feedDataProvider,
|
||||
name: r'feedDataProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$feedDataHash,
|
||||
dependencies: FeedDataFamily._dependencies,
|
||||
allTransitiveDependencies: FeedDataFamily._allTransitiveDependencies,
|
||||
feedId: feedId,
|
||||
);
|
||||
|
||||
FeedDataProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.feedId,
|
||||
}) : super.internal();
|
||||
|
||||
final Uri? feedId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
Stream<FeedData?> Function(FeedDataRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: FeedDataProvider._internal(
|
||||
(ref) => create(ref as FeedDataRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
feedId: feedId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeStreamProviderElement<FeedData?> createElement() {
|
||||
return _FeedDataProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FeedDataProvider && other.feedId == feedId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, feedId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin FeedDataRef on AutoDisposeStreamProviderRef<FeedData?> {
|
||||
/// The parameter `feedId` of this provider.
|
||||
Uri? get feedId;
|
||||
}
|
||||
|
||||
class _FeedDataProviderElement
|
||||
extends AutoDisposeStreamProviderElement<FeedData?>
|
||||
with FeedDataRef {
|
||||
_FeedDataProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Uri? get feedId => (origin as FeedDataProvider).feedId;
|
||||
}
|
||||
|
||||
String _$feedArticleListHash() => r'45d585cc9f59ad48a0d1d6fbcf802b1c7de7f6bc';
|
||||
|
||||
/// See also [feedArticleList].
|
||||
@ProviderFor(feedArticleList)
|
||||
const feedArticleListProvider = FeedArticleListFamily();
|
||||
@@ -55,15 +173,15 @@ class FeedArticleListFamily extends Family<AsyncValue<List<FeedArticle>>> {
|
||||
const FeedArticleListFamily();
|
||||
|
||||
/// See also [feedArticleList].
|
||||
FeedArticleListProvider call(FeedFilter filter) {
|
||||
return FeedArticleListProvider(filter);
|
||||
FeedArticleListProvider call(Uri? feedId) {
|
||||
return FeedArticleListProvider(feedId);
|
||||
}
|
||||
|
||||
@override
|
||||
FeedArticleListProvider getProviderOverride(
|
||||
covariant FeedArticleListProvider provider,
|
||||
) {
|
||||
return call(provider.filter);
|
||||
return call(provider.feedId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
@@ -85,9 +203,9 @@ class FeedArticleListFamily extends Family<AsyncValue<List<FeedArticle>>> {
|
||||
class FeedArticleListProvider
|
||||
extends AutoDisposeStreamProvider<List<FeedArticle>> {
|
||||
/// See also [feedArticleList].
|
||||
FeedArticleListProvider(FeedFilter filter)
|
||||
FeedArticleListProvider(Uri? feedId)
|
||||
: this._internal(
|
||||
(ref) => feedArticleList(ref as FeedArticleListRef, filter),
|
||||
(ref) => feedArticleList(ref as FeedArticleListRef, feedId),
|
||||
from: feedArticleListProvider,
|
||||
name: r'feedArticleListProvider',
|
||||
debugGetCreateSourceHash:
|
||||
@@ -97,7 +215,7 @@ class FeedArticleListProvider
|
||||
dependencies: FeedArticleListFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
FeedArticleListFamily._allTransitiveDependencies,
|
||||
filter: filter,
|
||||
feedId: feedId,
|
||||
);
|
||||
|
||||
FeedArticleListProvider._internal(
|
||||
@@ -107,10 +225,10 @@ class FeedArticleListProvider
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.filter,
|
||||
required this.feedId,
|
||||
}) : super.internal();
|
||||
|
||||
final FeedFilter filter;
|
||||
final Uri? feedId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
@@ -125,7 +243,7 @@ class FeedArticleListProvider
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
filter: filter,
|
||||
feedId: feedId,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -137,13 +255,13 @@ class FeedArticleListProvider
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FeedArticleListProvider && other.filter == filter;
|
||||
return other is FeedArticleListProvider && other.feedId == feedId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, filter.hashCode);
|
||||
hash = _SystemHash.combine(hash, feedId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
@@ -152,8 +270,8 @@ class FeedArticleListProvider
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin FeedArticleListRef on AutoDisposeStreamProviderRef<List<FeedArticle>> {
|
||||
/// The parameter `filter` of this provider.
|
||||
FeedFilter get filter;
|
||||
/// The parameter `feedId` of this provider.
|
||||
Uri? get feedId;
|
||||
}
|
||||
|
||||
class _FeedArticleListProviderElement
|
||||
@@ -162,10 +280,10 @@ class _FeedArticleListProviderElement
|
||||
_FeedArticleListProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
FeedFilter get filter => (origin as FeedArticleListProvider).filter;
|
||||
Uri? get feedId => (origin as FeedArticleListProvider).feedId;
|
||||
}
|
||||
|
||||
String _$feedArticleHash() => r'b1670f2ce11636f42fc0595befe42ada19319880';
|
||||
String _$feedArticleHash() => r'18b5faf391867b95f4b3bc4f74a6083854b633a5';
|
||||
|
||||
/// See also [feedArticle].
|
||||
@ProviderFor(feedArticle)
|
||||
@@ -177,15 +295,15 @@ class FeedArticleFamily extends Family<AsyncValue<FeedArticle?>> {
|
||||
const FeedArticleFamily();
|
||||
|
||||
/// See also [feedArticle].
|
||||
FeedArticleProvider call(String articleId) {
|
||||
return FeedArticleProvider(articleId);
|
||||
FeedArticleProvider call(String articleId, {required bool updateReadDate}) {
|
||||
return FeedArticleProvider(articleId, updateReadDate: updateReadDate);
|
||||
}
|
||||
|
||||
@override
|
||||
FeedArticleProvider getProviderOverride(
|
||||
covariant FeedArticleProvider provider,
|
||||
) {
|
||||
return call(provider.articleId);
|
||||
return call(provider.articleId, updateReadDate: provider.updateReadDate);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
@@ -206,9 +324,13 @@ class FeedArticleFamily extends Family<AsyncValue<FeedArticle?>> {
|
||||
/// See also [feedArticle].
|
||||
class FeedArticleProvider extends AutoDisposeStreamProvider<FeedArticle?> {
|
||||
/// See also [feedArticle].
|
||||
FeedArticleProvider(String articleId)
|
||||
FeedArticleProvider(String articleId, {required bool updateReadDate})
|
||||
: this._internal(
|
||||
(ref) => feedArticle(ref as FeedArticleRef, articleId),
|
||||
(ref) => feedArticle(
|
||||
ref as FeedArticleRef,
|
||||
articleId,
|
||||
updateReadDate: updateReadDate,
|
||||
),
|
||||
from: feedArticleProvider,
|
||||
name: r'feedArticleProvider',
|
||||
debugGetCreateSourceHash:
|
||||
@@ -218,6 +340,7 @@ class FeedArticleProvider extends AutoDisposeStreamProvider<FeedArticle?> {
|
||||
dependencies: FeedArticleFamily._dependencies,
|
||||
allTransitiveDependencies: FeedArticleFamily._allTransitiveDependencies,
|
||||
articleId: articleId,
|
||||
updateReadDate: updateReadDate,
|
||||
);
|
||||
|
||||
FeedArticleProvider._internal(
|
||||
@@ -228,9 +351,11 @@ class FeedArticleProvider extends AutoDisposeStreamProvider<FeedArticle?> {
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.articleId,
|
||||
required this.updateReadDate,
|
||||
}) : super.internal();
|
||||
|
||||
final String articleId;
|
||||
final bool updateReadDate;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
@@ -246,6 +371,7 @@ class FeedArticleProvider extends AutoDisposeStreamProvider<FeedArticle?> {
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
articleId: articleId,
|
||||
updateReadDate: updateReadDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -257,13 +383,16 @@ class FeedArticleProvider extends AutoDisposeStreamProvider<FeedArticle?> {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FeedArticleProvider && other.articleId == articleId;
|
||||
return other is FeedArticleProvider &&
|
||||
other.articleId == articleId &&
|
||||
other.updateReadDate == updateReadDate;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, articleId.hashCode);
|
||||
hash = _SystemHash.combine(hash, updateReadDate.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
@@ -274,6 +403,9 @@ class FeedArticleProvider extends AutoDisposeStreamProvider<FeedArticle?> {
|
||||
mixin FeedArticleRef on AutoDisposeStreamProviderRef<FeedArticle?> {
|
||||
/// The parameter `articleId` of this provider.
|
||||
String get articleId;
|
||||
|
||||
/// The parameter `updateReadDate` of this provider.
|
||||
bool get updateReadDate;
|
||||
}
|
||||
|
||||
class _FeedArticleProviderElement
|
||||
@@ -283,17 +415,19 @@ class _FeedArticleProviderElement
|
||||
|
||||
@override
|
||||
String get articleId => (origin as FeedArticleProvider).articleId;
|
||||
@override
|
||||
bool get updateReadDate => (origin as FeedArticleProvider).updateReadDate;
|
||||
}
|
||||
|
||||
String _$unreadArticleCountHash() =>
|
||||
r'6fb96215fb3b7739a6cbd594a0358415ced04fca';
|
||||
r'709518ad229636df0f1095f47e3a6d116b3aa7e6';
|
||||
|
||||
/// See also [_unreadArticleCount].
|
||||
@ProviderFor(_unreadArticleCount)
|
||||
final _unreadArticleCountProvider =
|
||||
/// See also [unreadArticleCount].
|
||||
@ProviderFor(unreadArticleCount)
|
||||
final unreadArticleCountProvider =
|
||||
AutoDisposeProvider<Raw<Stream<Map<String, int>>>>.internal(
|
||||
_unreadArticleCount,
|
||||
name: r'_unreadArticleCountProvider',
|
||||
unreadArticleCount,
|
||||
name: r'unreadArticleCountProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
@@ -304,10 +438,10 @@ final _unreadArticleCountProvider =
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef _UnreadArticleCountRef =
|
||||
typedef UnreadArticleCountRef =
|
||||
AutoDisposeProviderRef<Raw<Stream<Map<String, int>>>>;
|
||||
String _$unreadFeedArticleCountHash() =>
|
||||
r'f8573674477f0d9813b42c82d75e8196c1c1445b';
|
||||
r'5e0d8e58b3d1dec978dc07b22367f2cb037b1b15';
|
||||
|
||||
/// See also [unreadFeedArticleCount].
|
||||
@ProviderFor(unreadFeedArticleCount)
|
||||
@@ -429,5 +563,416 @@ class _UnreadFeedArticleCountProviderElement
|
||||
Uri get feedId => (origin as UnreadFeedArticleCountProvider).feedId;
|
||||
}
|
||||
|
||||
String _$fetchWebFeedHash() => r'73bdf87ad7dbd039c7dc181d80acdf99d96fe1c6';
|
||||
|
||||
/// See also [fetchWebFeed].
|
||||
@ProviderFor(fetchWebFeed)
|
||||
const fetchWebFeedProvider = FetchWebFeedFamily();
|
||||
|
||||
/// See also [fetchWebFeed].
|
||||
class FetchWebFeedFamily extends Family<AsyncValue<FeedParseResult>> {
|
||||
/// See also [fetchWebFeed].
|
||||
const FetchWebFeedFamily();
|
||||
|
||||
/// See also [fetchWebFeed].
|
||||
FetchWebFeedProvider call(Uri url) {
|
||||
return FetchWebFeedProvider(url);
|
||||
}
|
||||
|
||||
@override
|
||||
FetchWebFeedProvider getProviderOverride(
|
||||
covariant FetchWebFeedProvider provider,
|
||||
) {
|
||||
return call(provider.url);
|
||||
}
|
||||
|
||||
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'fetchWebFeedProvider';
|
||||
}
|
||||
|
||||
/// See also [fetchWebFeed].
|
||||
class FetchWebFeedProvider extends AutoDisposeFutureProvider<FeedParseResult> {
|
||||
/// See also [fetchWebFeed].
|
||||
FetchWebFeedProvider(Uri url)
|
||||
: this._internal(
|
||||
(ref) => fetchWebFeed(ref as FetchWebFeedRef, url),
|
||||
from: fetchWebFeedProvider,
|
||||
name: r'fetchWebFeedProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$fetchWebFeedHash,
|
||||
dependencies: FetchWebFeedFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
FetchWebFeedFamily._allTransitiveDependencies,
|
||||
url: url,
|
||||
);
|
||||
|
||||
FetchWebFeedProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.url,
|
||||
}) : super.internal();
|
||||
|
||||
final Uri url;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<FeedParseResult> Function(FetchWebFeedRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: FetchWebFeedProvider._internal(
|
||||
(ref) => create(ref as FetchWebFeedRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<FeedParseResult> createElement() {
|
||||
return _FetchWebFeedProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FetchWebFeedProvider && other.url == url;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, url.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin FetchWebFeedRef on AutoDisposeFutureProviderRef<FeedParseResult> {
|
||||
/// The parameter `url` of this provider.
|
||||
Uri get url;
|
||||
}
|
||||
|
||||
class _FetchWebFeedProviderElement
|
||||
extends AutoDisposeFutureProviderElement<FeedParseResult>
|
||||
with FetchWebFeedRef {
|
||||
_FetchWebFeedProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Uri get url => (origin as FetchWebFeedProvider).url;
|
||||
}
|
||||
|
||||
String _$articleSearchHash() => r'8bf2c4aa8d8b3918be8a416909535682abfbab35';
|
||||
|
||||
abstract class _$ArticleSearch
|
||||
extends BuildlessAutoDisposeStreamNotifier<List<FeedArticle>> {
|
||||
late final Uri? feedId;
|
||||
|
||||
Stream<List<FeedArticle>> build(Uri? feedId);
|
||||
}
|
||||
|
||||
/// See also [ArticleSearch].
|
||||
@ProviderFor(ArticleSearch)
|
||||
const articleSearchProvider = ArticleSearchFamily();
|
||||
|
||||
/// See also [ArticleSearch].
|
||||
class ArticleSearchFamily extends Family<AsyncValue<List<FeedArticle>>> {
|
||||
/// See also [ArticleSearch].
|
||||
const ArticleSearchFamily();
|
||||
|
||||
/// See also [ArticleSearch].
|
||||
ArticleSearchProvider call(Uri? feedId) {
|
||||
return ArticleSearchProvider(feedId);
|
||||
}
|
||||
|
||||
@override
|
||||
ArticleSearchProvider getProviderOverride(
|
||||
covariant ArticleSearchProvider provider,
|
||||
) {
|
||||
return call(provider.feedId);
|
||||
}
|
||||
|
||||
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'articleSearchProvider';
|
||||
}
|
||||
|
||||
/// See also [ArticleSearch].
|
||||
class ArticleSearchProvider
|
||||
extends
|
||||
AutoDisposeStreamNotifierProviderImpl<
|
||||
ArticleSearch,
|
||||
List<FeedArticle>
|
||||
> {
|
||||
/// See also [ArticleSearch].
|
||||
ArticleSearchProvider(Uri? feedId)
|
||||
: this._internal(
|
||||
() => ArticleSearch()..feedId = feedId,
|
||||
from: articleSearchProvider,
|
||||
name: r'articleSearchProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$articleSearchHash,
|
||||
dependencies: ArticleSearchFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ArticleSearchFamily._allTransitiveDependencies,
|
||||
feedId: feedId,
|
||||
);
|
||||
|
||||
ArticleSearchProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.feedId,
|
||||
}) : super.internal();
|
||||
|
||||
final Uri? feedId;
|
||||
|
||||
@override
|
||||
Stream<List<FeedArticle>> runNotifierBuild(covariant ArticleSearch notifier) {
|
||||
return notifier.build(feedId);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(ArticleSearch Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ArticleSearchProvider._internal(
|
||||
() => create()..feedId = feedId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
feedId: feedId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeStreamNotifierProviderElement<ArticleSearch, List<FeedArticle>>
|
||||
createElement() {
|
||||
return _ArticleSearchProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ArticleSearchProvider && other.feedId == feedId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, feedId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ArticleSearchRef
|
||||
on AutoDisposeStreamNotifierProviderRef<List<FeedArticle>> {
|
||||
/// The parameter `feedId` of this provider.
|
||||
Uri? get feedId;
|
||||
}
|
||||
|
||||
class _ArticleSearchProviderElement
|
||||
extends
|
||||
AutoDisposeStreamNotifierProviderElement<
|
||||
ArticleSearch,
|
||||
List<FeedArticle>
|
||||
>
|
||||
with ArticleSearchRef {
|
||||
_ArticleSearchProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Uri? get feedId => (origin as ArticleSearchProvider).feedId;
|
||||
}
|
||||
|
||||
String _$filteredArticleListHash() =>
|
||||
r'4443a4a92cab0c8f534a74a1cba681a8973b058e';
|
||||
|
||||
abstract class _$FilteredArticleList
|
||||
extends BuildlessAutoDisposeNotifier<AsyncValue<List<FeedArticle>>> {
|
||||
late final Uri? feedId;
|
||||
|
||||
AsyncValue<List<FeedArticle>> build(Uri? feedId);
|
||||
}
|
||||
|
||||
/// See also [FilteredArticleList].
|
||||
@ProviderFor(FilteredArticleList)
|
||||
const filteredArticleListProvider = FilteredArticleListFamily();
|
||||
|
||||
/// See also [FilteredArticleList].
|
||||
class FilteredArticleListFamily extends Family<AsyncValue<List<FeedArticle>>> {
|
||||
/// See also [FilteredArticleList].
|
||||
const FilteredArticleListFamily();
|
||||
|
||||
/// See also [FilteredArticleList].
|
||||
FilteredArticleListProvider call(Uri? feedId) {
|
||||
return FilteredArticleListProvider(feedId);
|
||||
}
|
||||
|
||||
@override
|
||||
FilteredArticleListProvider getProviderOverride(
|
||||
covariant FilteredArticleListProvider provider,
|
||||
) {
|
||||
return call(provider.feedId);
|
||||
}
|
||||
|
||||
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'filteredArticleListProvider';
|
||||
}
|
||||
|
||||
/// See also [FilteredArticleList].
|
||||
class FilteredArticleListProvider
|
||||
extends
|
||||
AutoDisposeNotifierProviderImpl<
|
||||
FilteredArticleList,
|
||||
AsyncValue<List<FeedArticle>>
|
||||
> {
|
||||
/// See also [FilteredArticleList].
|
||||
FilteredArticleListProvider(Uri? feedId)
|
||||
: this._internal(
|
||||
() => FilteredArticleList()..feedId = feedId,
|
||||
from: filteredArticleListProvider,
|
||||
name: r'filteredArticleListProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$filteredArticleListHash,
|
||||
dependencies: FilteredArticleListFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
FilteredArticleListFamily._allTransitiveDependencies,
|
||||
feedId: feedId,
|
||||
);
|
||||
|
||||
FilteredArticleListProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.feedId,
|
||||
}) : super.internal();
|
||||
|
||||
final Uri? feedId;
|
||||
|
||||
@override
|
||||
AsyncValue<List<FeedArticle>> runNotifierBuild(
|
||||
covariant FilteredArticleList notifier,
|
||||
) {
|
||||
return notifier.build(feedId);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(FilteredArticleList Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: FilteredArticleListProvider._internal(
|
||||
() => create()..feedId = feedId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
feedId: feedId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<
|
||||
FilteredArticleList,
|
||||
AsyncValue<List<FeedArticle>>
|
||||
>
|
||||
createElement() {
|
||||
return _FilteredArticleListProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FilteredArticleListProvider && other.feedId == feedId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, feedId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin FilteredArticleListRef
|
||||
on AutoDisposeNotifierProviderRef<AsyncValue<List<FeedArticle>>> {
|
||||
/// The parameter `feedId` of this provider.
|
||||
Uri? get feedId;
|
||||
}
|
||||
|
||||
class _FilteredArticleListProviderElement
|
||||
extends
|
||||
AutoDisposeNotifierProviderElement<
|
||||
FilteredArticleList,
|
||||
AsyncValue<List<FeedArticle>>
|
||||
>
|
||||
with FilteredArticleListRef {
|
||||
_FilteredArticleListProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Uri? get feedId => (origin as FilteredArticleListProvider).feedId;
|
||||
}
|
||||
|
||||
// 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,5 +1,3 @@
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_filter.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'article_filter.g.dart';
|
||||
@@ -7,20 +5,17 @@ part 'article_filter.g.dart';
|
||||
@Riverpod(keepAlive: true)
|
||||
class ArticleFilter extends _$ArticleFilter {
|
||||
void addTag(String tagId) {
|
||||
final tags = {...?state.tags, tagId};
|
||||
|
||||
state = state.copyWith.tags(tags);
|
||||
state = {...state, tagId};
|
||||
}
|
||||
|
||||
void removeTag(String tagId) {
|
||||
if (state.tags.isNotEmpty) {
|
||||
final tags = {...state.tags!}..remove(tagId);
|
||||
state = state.copyWith.tags(tags);
|
||||
if (state.isNotEmpty) {
|
||||
state = {...state}..remove(tagId);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
FeedFilter build() {
|
||||
return FeedFilter();
|
||||
Set<String> build() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ part of 'article_filter.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$articleFilterHash() => r'dfc997af8a33cbcb995288ef633ff321e95bae8d';
|
||||
String _$articleFilterHash() => r'61e4d5230e214038e753bc173158ed1d4dc57040';
|
||||
|
||||
/// See also [ArticleFilter].
|
||||
@ProviderFor(ArticleFilter)
|
||||
final articleFilterProvider =
|
||||
NotifierProvider<ArticleFilter, FeedFilter>.internal(
|
||||
NotifierProvider<ArticleFilter, Set<String>>.internal(
|
||||
ArticleFilter.new,
|
||||
name: r'articleFilterProvider',
|
||||
debugGetCreateSourceHash:
|
||||
@@ -22,6 +22,6 @@ final articleFilterProvider =
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ArticleFilter = Notifier<FeedFilter>;
|
||||
typedef _$ArticleFilter = Notifier<Set<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
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/web_feed/data/database/database.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_filter.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_article.dart';
|
||||
import 'package:lensai/features/web_feed/data/providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -13,11 +11,11 @@ class FeedRepository extends _$FeedRepository {
|
||||
return ref.read(feedDatabaseProvider).feedDao.getFeeds().get();
|
||||
}
|
||||
|
||||
Future<void> touchFeedFetched(Uri url) {
|
||||
Future<void> touchFeedFetched(Uri feedId) {
|
||||
return ref
|
||||
.read(feedDatabaseProvider)
|
||||
.feedDao
|
||||
.updateFeedFetched(url, DateTime.now());
|
||||
.updateFeedFetched(feedId, DateTime.now());
|
||||
}
|
||||
|
||||
Future<void> upsertFeed(FeedData feedData) {
|
||||
@@ -28,8 +26,8 @@ class FeedRepository extends _$FeedRepository {
|
||||
return ref.read(feedDatabaseProvider).articleDao.upsertArticles(articles);
|
||||
}
|
||||
|
||||
Future<int> deleteFeed(Uri url) {
|
||||
return ref.read(feedDatabaseProvider).feedDao.deleteFeed(url);
|
||||
Future<int> deleteFeed(Uri feedId) {
|
||||
return ref.read(feedDatabaseProvider).feedDao.deleteFeed(feedId);
|
||||
}
|
||||
|
||||
Future<void> touchArticleRead(String articleId) {
|
||||
@@ -50,46 +48,20 @@ class FeedRepository extends _$FeedRepository {
|
||||
return ref.read(feedDatabaseProvider).feedDao.getFeeds().watch();
|
||||
}
|
||||
|
||||
Stream<List<FeedArticle>> watchFeedArticles(
|
||||
FeedFilter filter, {
|
||||
int snippetLength = 120,
|
||||
String matchPrefix = '***',
|
||||
String matchSuffix = '***',
|
||||
String ellipsis = '…',
|
||||
}) {
|
||||
final stream =
|
||||
filter.query.isNotEmpty
|
||||
? ref
|
||||
.read(feedDatabaseProvider)
|
||||
.articleDao
|
||||
.queryArticles(
|
||||
matchPrefix: matchPrefix,
|
||||
matchSuffix: matchSuffix,
|
||||
ellipsis: ellipsis,
|
||||
snippetLength: snippetLength,
|
||||
searchString: filter.query!,
|
||||
feedId: filter.feedId,
|
||||
)
|
||||
.watch()
|
||||
: ref
|
||||
.read(feedDatabaseProvider)
|
||||
.articleDao
|
||||
.getFeedArticles(filter.feedId)
|
||||
.watch();
|
||||
Stream<FeedData?> watchFeed(Uri feedId) {
|
||||
return ref
|
||||
.read(feedDatabaseProvider)
|
||||
.feedDao
|
||||
.getFeed(feedId)
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
if (filter.tags.isNotEmpty) {
|
||||
return stream.map(
|
||||
(articles) =>
|
||||
articles
|
||||
.where(
|
||||
(article) =>
|
||||
article.tags?.toSet().containsAll(filter.tags!) ?? false,
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
return stream;
|
||||
}
|
||||
Stream<List<FeedArticle>> watchFeedArticles(Uri? feedId) {
|
||||
return ref
|
||||
.read(feedDatabaseProvider)
|
||||
.articleDao
|
||||
.getFeedArticles(feedId)
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<FeedArticle?> watchArticle(String articleId) {
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'feed_repository.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$feedRepositoryHash() => r'6a050933a6a4eafbe76a66262e1b64bd838a1078';
|
||||
String _$feedRepositoryHash() => r'805cc26890b0d43576a1eab0ecb5851b6649d7b7';
|
||||
|
||||
/// See also [FeedRepository].
|
||||
@ProviderFor(FeedRepository)
|
||||
|
||||
@@ -26,6 +26,12 @@ extension ParseAtomLink on List<AtomLink> {
|
||||
}
|
||||
}
|
||||
|
||||
extension SelectFeedLink on List<FeedLink> {
|
||||
FeedLink? getRelation(FeedLinkRelation relation) {
|
||||
return firstWhereOrNull((link) => link.relation == relation);
|
||||
}
|
||||
}
|
||||
|
||||
extension ParseAtomCategory on List<AtomCategory> {
|
||||
List<FeedCategory> toFeedCategories() {
|
||||
return where((category) => category.term.isNotEmpty)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_article.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_link.dart';
|
||||
import 'package:lensai/features/web_feed/extensions/atom.dart';
|
||||
|
||||
extension FeedArticleX on FeedArticle {
|
||||
String get displayTitle =>
|
||||
title ??
|
||||
links
|
||||
?.firstWhereOrNull(
|
||||
(link) => link.relation == FeedLinkRelation.alternate,
|
||||
)
|
||||
?.getRelation(FeedLinkRelation.alternate)
|
||||
.mapNotNull(
|
||||
(link) => link.title.whenNotEmpty ?? link.uri.toString(),
|
||||
) ??
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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/utils/form_validators.dart';
|
||||
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
||||
|
||||
class AddFeedDialog extends HookConsumerWidget {
|
||||
final Uri? initialUri;
|
||||
|
||||
const AddFeedDialog({super.key, required this.initialUri});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
|
||||
final textController = useTextEditingController(
|
||||
text: initialUri?.toString(),
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('Add Feed'),
|
||||
// contentPadding: const EdgeInsets.fromLTRB(24.0, 12.0, 24.0, 16.0),
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
label: Text('URL'),
|
||||
hintText: 'https://example.com/feed',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
return validateUrl(value, onlyHttpProtocol: true);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (formKey.currentState?.validate() == true) {
|
||||
FeedCreateRoute(
|
||||
feedId: uri_parser.tryParseUrl(textController.text)!,
|
||||
).pushReplacement(context);
|
||||
}
|
||||
},
|
||||
child: const Text('Add'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/providers/format.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
@@ -10,6 +9,7 @@ import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/tab.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/features/web_feed/presentation/widgets/authors_horizontal_list.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/widgets/tags_horizontal_list.dart';
|
||||
@@ -26,13 +26,16 @@ class FeedArticleScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final articleAsync = ref.watch(feedArticleProvider(articleId));
|
||||
final articleAsync = ref.watch(
|
||||
feedArticleProvider(articleId, updateReadDate: true),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: articleAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (article) {
|
||||
if (article == null) {
|
||||
return SizedBox.shrink();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return HookBuilder(
|
||||
@@ -55,9 +58,7 @@ class FeedArticleScreen extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final articleLink = useMemoized(
|
||||
() => article.links?.firstWhereOrNull(
|
||||
(link) => link.relation == FeedLinkRelation.alternate,
|
||||
),
|
||||
() => article.links?.getRelation(FeedLinkRelation.alternate),
|
||||
);
|
||||
|
||||
final articleImages = useMemoized(
|
||||
@@ -176,7 +177,7 @@ class FeedArticleScreen extends HookConsumerWidget {
|
||||
.addTab(url: articleLink.uri);
|
||||
|
||||
if (context.mounted) {
|
||||
context.go(BrowserRoute().location);
|
||||
BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.open_in_browser),
|
||||
@@ -218,7 +219,7 @@ class FeedArticleScreen extends HookConsumerWidget {
|
||||
context,
|
||||
tabName: title.whenNotEmpty,
|
||||
onShow: () {
|
||||
context.go(BrowserRoute().location);
|
||||
BrowserRoute().go(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_filter.dart';
|
||||
import 'package:lensai/features/web_feed/domain/providers.dart';
|
||||
import 'package:lensai/features/web_feed/domain/providers/article_filter.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/controllers/fetch_articles.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/widgets/feed_article_card.dart';
|
||||
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||
import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
|
||||
|
||||
class FeedArticleListScreen extends HookConsumerWidget {
|
||||
final Uri? feedId;
|
||||
@@ -17,82 +18,170 @@ class FeedArticleListScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final articlesAsync = ref.watch(
|
||||
// ignore: provider_parameters
|
||||
feedArticleListProvider(FeedFilter(feedId: feedId)),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: NestedScrollView(
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
Consumer(
|
||||
HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final tags = ref.watch(articleFilterProvider);
|
||||
final feedTitle = ref.watch(
|
||||
feedDataProvider(
|
||||
feedId,
|
||||
).select((value) => value.valueOrNull?.title.whenNotEmpty),
|
||||
);
|
||||
|
||||
return SliverAppBar(floating: true, title: Text('Articles'));
|
||||
final searchTextController = useTextEditingController();
|
||||
|
||||
final hasText = useListenableSelector(
|
||||
searchTextController,
|
||||
() => searchTextController.text.isNotEmpty,
|
||||
);
|
||||
|
||||
useListenableCallback(searchTextController, () {
|
||||
ref
|
||||
.read(filteredArticleListProvider(feedId).notifier)
|
||||
.search(searchTextController.text);
|
||||
});
|
||||
|
||||
final bottomHeight = useMemoized(() {
|
||||
var height = 56.0 + 4.0;
|
||||
|
||||
if (tags.isNotEmpty) {
|
||||
height += 48;
|
||||
}
|
||||
|
||||
return height;
|
||||
}, [tags.isNotEmpty]);
|
||||
|
||||
return SliverAppBar(
|
||||
floating: true,
|
||||
title: Text(feedTitle ?? 'Articles'),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size(double.infinity, bottomHeight),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: searchTextController,
|
||||
decoration: InputDecoration(
|
||||
label: const Text('Search'),
|
||||
suffixIcon:
|
||||
hasText
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
searchTextController.clear();
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
)
|
||||
: SpeechToTextButton(
|
||||
onTextReceived: (data) {
|
||||
searchTextController.text =
|
||||
data.toString();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (tags.isNotEmpty)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: FadingScroll(
|
||||
fadingSize: 15,
|
||||
builder: (context, controller) {
|
||||
return ListView(
|
||||
controller: controller,
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
children:
|
||||
tags
|
||||
.map(
|
||||
(tag) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 8.0,
|
||||
),
|
||||
child: FilterChip(
|
||||
label: Text(tag),
|
||||
showCheckmark: false,
|
||||
selected: true,
|
||||
onSelected: (value) {},
|
||||
onDeleted: () {
|
||||
ref
|
||||
.read(
|
||||
articleFilterProvider
|
||||
.notifier,
|
||||
)
|
||||
.removeTag(tag);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
body: articlesAsync.when(
|
||||
data: (articles) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
if (feedId != null) {
|
||||
await ref
|
||||
.read(fetchArticlesControllerProvider.notifier)
|
||||
.fetchFeedArticles(feedId!);
|
||||
} else {
|
||||
await ref
|
||||
.read(fetchArticlesControllerProvider.notifier)
|
||||
.fetchAllArticles();
|
||||
}
|
||||
body: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final articlesAsync = ref.watch(
|
||||
// ignore: provider_parameters
|
||||
filteredArticleListProvider(feedId),
|
||||
);
|
||||
|
||||
return articlesAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (articles) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
if (feedId != null) {
|
||||
await ref
|
||||
.read(fetchArticlesControllerProvider.notifier)
|
||||
.fetchFeedArticles(feedId!);
|
||||
} else {
|
||||
await ref
|
||||
.read(fetchArticlesControllerProvider.notifier)
|
||||
.fetchAllArticles();
|
||||
}
|
||||
},
|
||||
child: MediaQuery.removePadding(
|
||||
removeTop: true,
|
||||
context: context,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: articles.length,
|
||||
itemBuilder: (context, i) {
|
||||
final article = articles[i];
|
||||
return FeedArticleCard(
|
||||
key: ValueKey(article.id),
|
||||
article: article,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: articles.length,
|
||||
itemBuilder: (context, i) {
|
||||
final article = articles[i];
|
||||
|
||||
return Consumer(
|
||||
key: ValueKey(article.id),
|
||||
builder: (context, ref, child) {
|
||||
final tags = ref.watch(
|
||||
articleFilterProvider.select(
|
||||
(value) => EquatableValue(value.tags ?? const {}),
|
||||
),
|
||||
);
|
||||
|
||||
return FeedArticleCard(
|
||||
selectedTags: tags.value,
|
||||
onTagSelected: (tagId, value) {
|
||||
if (value) {
|
||||
ref
|
||||
.read(articleFilterProvider.notifier)
|
||||
.addTag(tagId);
|
||||
} else {
|
||||
ref
|
||||
.read(articleFilterProvider.notifier)
|
||||
.removeTag(tagId);
|
||||
}
|
||||
},
|
||||
article: article,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
error:
|
||||
(error, stackTrace) => Center(
|
||||
child: FailureWidget(
|
||||
title: 'Failed to load Articles',
|
||||
exception: error,
|
||||
),
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
error:
|
||||
(error, stackTrace) => Center(
|
||||
child: FailureWidget(
|
||||
title: 'Failed to load Articles',
|
||||
exception: error,
|
||||
),
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,33 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/extensions/uri.dart';
|
||||
import 'package:lensai/features/web_feed/data/database/database.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_category.dart';
|
||||
import 'package:lensai/features/web_feed/domain/providers.dart';
|
||||
import 'package:lensai/features/web_feed/domain/repositories/feed_repository.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/widgets/tag_field.dart';
|
||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||
import 'package:lensai/presentation/widgets/url_icon.dart';
|
||||
import 'package:lensai/utils/form_validators.dart';
|
||||
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
||||
|
||||
enum _DialogMode { create, edit }
|
||||
|
||||
class FeedEditScreen extends HookConsumerWidget {
|
||||
final _DialogMode _mode;
|
||||
|
||||
final FeedData initialFeed;
|
||||
final Uri feedId;
|
||||
|
||||
const FeedEditScreen._({required _DialogMode mode, required this.initialFeed})
|
||||
const FeedEditScreen._({required _DialogMode mode, required this.feedId})
|
||||
: _mode = mode;
|
||||
|
||||
factory FeedEditScreen.create({required FeedData initialFeed}) {
|
||||
return FeedEditScreen._(mode: _DialogMode.create, initialFeed: initialFeed);
|
||||
factory FeedEditScreen.create({required Uri feedId}) {
|
||||
return FeedEditScreen._(mode: _DialogMode.create, feedId: feedId);
|
||||
}
|
||||
|
||||
factory FeedEditScreen.edit({required FeedData initialFeed}) {
|
||||
return FeedEditScreen._(mode: _DialogMode.edit, initialFeed: initialFeed);
|
||||
factory FeedEditScreen.edit({required Uri feedId}) {
|
||||
return FeedEditScreen._(mode: _DialogMode.edit, feedId: feedId);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final initialFeedAsync = switch (_mode) {
|
||||
_DialogMode.create => ref.watch(
|
||||
fetchWebFeedProvider(
|
||||
feedId,
|
||||
).select((value) => value.whenData((result) => result.feedData)),
|
||||
),
|
||||
_DialogMode.edit => ref.watch(feedDataProvider(feedId)),
|
||||
};
|
||||
|
||||
return initialFeedAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (initialFeed) {
|
||||
if (initialFeed == null) {
|
||||
return Scaffold(
|
||||
key: const ValueKey('data'),
|
||||
appBar: AppBar(),
|
||||
body: const Center(
|
||||
child: FailureWidget(title: 'Failed to load feed'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _FeedEditContent(mode: _mode, initialFeed: initialFeed);
|
||||
},
|
||||
error:
|
||||
(error, stackTrace) => Scaffold(
|
||||
key: const ValueKey('error'),
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: FailureWidget(
|
||||
title: 'Failed to load feed',
|
||||
exception: error,
|
||||
),
|
||||
),
|
||||
),
|
||||
loading:
|
||||
() => Scaffold(
|
||||
key: const ValueKey('loading'),
|
||||
appBar: AppBar(
|
||||
title: Text(switch (_mode) {
|
||||
_DialogMode.create => 'New Feed',
|
||||
_DialogMode.edit => 'Edit Feed',
|
||||
}),
|
||||
),
|
||||
body: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 8.0),
|
||||
child: Text('Fetching feed...'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FeedEditContent extends HookConsumerWidget {
|
||||
final _DialogMode _mode;
|
||||
|
||||
final FeedData initialFeed;
|
||||
|
||||
const _FeedEditContent({required _DialogMode mode, required this.initialFeed})
|
||||
: _mode = mode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
@@ -45,6 +121,12 @@ class FeedEditScreen extends HookConsumerWidget {
|
||||
final urlTextController = useTextEditingController(
|
||||
text: initialFeed.url.toString(),
|
||||
);
|
||||
final iconUrlTextController = useTextEditingController(
|
||||
text: initialFeed.icon?.toString(),
|
||||
);
|
||||
final siteLinkTextController = useTextEditingController(
|
||||
text: initialFeed.siteLink?.toString(),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -57,9 +139,21 @@ class FeedEditScreen extends HookConsumerWidget {
|
||||
onPressed: () async {
|
||||
if (formKey.currentState?.validate() ?? false) {
|
||||
final feedData = FeedData(
|
||||
url: Uri.parse(urlTextController.text),
|
||||
url:
|
||||
uri_parser.tryParseUrl(
|
||||
urlTextController.text,
|
||||
eagerParsing: true,
|
||||
)!,
|
||||
authors: initialFeed.authors,
|
||||
description: descriptionTextController.text.whenNotEmpty,
|
||||
icon: uri_parser.tryParseUrl(
|
||||
iconUrlTextController.text,
|
||||
eagerParsing: true,
|
||||
),
|
||||
siteLink: uri_parser.tryParseUrl(
|
||||
siteLinkTextController.text,
|
||||
eagerParsing: true,
|
||||
),
|
||||
tags: tags.value.map((tag) => FeedCategory(id: tag)).toList(),
|
||||
title: titleTextController.text.whenNotEmpty,
|
||||
);
|
||||
@@ -91,10 +185,11 @@ class FeedEditScreen extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: UrlIcon(
|
||||
initialFeed.url.base,
|
||||
iconSize: 24.0,
|
||||
),
|
||||
child: UrlIcon([
|
||||
initialFeed.icon ??
|
||||
initialFeed.siteLink ??
|
||||
initialFeed.url.base,
|
||||
], iconSize: 24.0),
|
||||
),
|
||||
label: const Text('Title'),
|
||||
),
|
||||
@@ -103,40 +198,63 @@ class FeedEditScreen extends HookConsumerWidget {
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
label: Text('Description'),
|
||||
prefixIcon: Icon(Icons.short_text),
|
||||
),
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
controller: descriptionTextController,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 32),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
label: Text('Icon URL'),
|
||||
prefixIcon: Icon(Icons.image),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
controller: iconUrlTextController,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (value) {
|
||||
return validateUrl(
|
||||
value,
|
||||
onlyHttpProtocol: true,
|
||||
required: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
label: Text('Site Link'),
|
||||
prefixIcon: Icon(Icons.link),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
controller: siteLinkTextController,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (value) {
|
||||
return validateUrl(
|
||||
value,
|
||||
onlyHttpProtocol: true,
|
||||
required: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TagField(
|
||||
initialTags: tags.value,
|
||||
onTagsUpdate: (newTags) {
|
||||
tags.value = newTags;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 32),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
label: Text('Address'),
|
||||
label: Text('Feed URL'),
|
||||
prefixIcon: Icon(MdiIcons.rss),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
controller: urlTextController,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return 'Address must be provided';
|
||||
}
|
||||
|
||||
if (Uri.tryParse(value!) case final Uri url) {
|
||||
if (url.isScheme('https') ||
|
||||
url.isScheme('http') &&
|
||||
url.authority.isNotEmpty) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return 'Inavlid URL';
|
||||
return validateUrl(value, onlyHttpProtocol: true);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
import 'package:lensai/features/web_feed/domain/providers.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/controllers/fetch_articles.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/widgets/feed_card.dart';
|
||||
@@ -15,6 +16,7 @@ class FeedListScreen extends HookConsumerWidget {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Feeds')),
|
||||
body: feeds.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (feeds) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
@@ -35,10 +37,21 @@ class FeedListScreen extends HookConsumerWidget {
|
||||
child: FailureWidget(
|
||||
title: 'Failed to load Feeds',
|
||||
exception: error,
|
||||
onRetry: () {
|
||||
// ignore: unused_result
|
||||
ref.refresh(feedListProvider);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
label: const Text('Feed'),
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
await const FeedAddRoute().push(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/web_feed/domain/providers.dart';
|
||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class SelectFeedDialog extends HookConsumerWidget {
|
||||
final Set<Uri> feedUris;
|
||||
|
||||
const SelectFeedDialog({required this.feedUris});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SimpleDialog(
|
||||
title: const Text('Add Feed'),
|
||||
children:
|
||||
feedUris
|
||||
.map(
|
||||
(uri) => HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final feedAsync = ref.watch(fetchWebFeedProvider(uri));
|
||||
|
||||
return feedAsync.when(
|
||||
data: (data) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
data.feedData.title.whenNotEmpty ?? 'Unnamed Feed',
|
||||
),
|
||||
subtitle: Text(uri.toString()),
|
||||
trailing: const Icon(Icons.add),
|
||||
onTap: () {
|
||||
FeedCreateRoute(
|
||||
feedId: uri,
|
||||
).pushReplacement(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
error:
|
||||
(error, stackTrace) => FailureWidget(
|
||||
title: 'Failed to fetch Feed',
|
||||
exception: error,
|
||||
onRetry: () {
|
||||
// ignore: unused_result
|
||||
ref.refresh(fetchWebFeedProvider(uri));
|
||||
},
|
||||
),
|
||||
loading:
|
||||
() => Skeletonizer(
|
||||
child: ListTile(
|
||||
title: Text(BoneMock.title),
|
||||
subtitle: Skeleton.keep(
|
||||
child: Text(uri.toString()),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,31 @@ import 'package:lensai/features/web_feed/data/models/feed_author.dart';
|
||||
class AuthorsHorizontalList extends StatelessWidget {
|
||||
late final List<Widget> _authors;
|
||||
|
||||
AuthorsHorizontalList({required List<FeedAuthor> authors}) {
|
||||
AuthorsHorizontalList({
|
||||
required List<FeedAuthor> authors,
|
||||
Set<String> selectedTags = const {},
|
||||
void Function(String tagId, bool value)? onTagSelected,
|
||||
}) {
|
||||
_authors =
|
||||
authors
|
||||
.map(
|
||||
(author) => Chip(
|
||||
label: Text(
|
||||
'${author.name ?? ''} ${author.email.mapNotNull((email) => '($email)') ?? ''}'
|
||||
.trim(),
|
||||
authors.map((author) {
|
||||
final label = Text(
|
||||
'${author.name ?? ''} ${author.email.mapNotNull((email) => '($email)') ?? ''}'
|
||||
.trim(),
|
||||
);
|
||||
|
||||
return onTagSelected.mapNotNull(
|
||||
(onTagSelected) => FilterChip(
|
||||
label: label,
|
||||
selected: selectedTags.contains(author.name),
|
||||
onSelected: (value) {
|
||||
if (author.name.isNotEmpty) {
|
||||
onTagSelected(author.name!, value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
) ??
|
||||
Chip(label: label);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -30,7 +43,6 @@ class AuthorsHorizontalList extends StatelessWidget {
|
||||
return ListView.builder(
|
||||
itemCount: _authors.length,
|
||||
controller: controller,
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => _authors[index],
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.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.dart';
|
||||
import 'package:lensai/features/web_feed/data/models/feed_article_query_result.dart';
|
||||
import 'package:lensai/features/web_feed/domain/providers/article_filter.dart';
|
||||
import 'package:lensai/features/web_feed/domain/repositories/feed_repository.dart';
|
||||
import 'package:lensai/features/web_feed/extensions/feed_article.dart';
|
||||
import 'package:lensai/features/web_feed/presentation/widgets/authors_horizontal_list.dart';
|
||||
@@ -15,34 +17,31 @@ import 'package:timeago/timeago.dart' as timeago;
|
||||
class FeedArticleCard extends HookConsumerWidget {
|
||||
final FeedArticle article;
|
||||
|
||||
final Set<String> selectedTags;
|
||||
final void Function(String tagId, bool value)? onTagSelected;
|
||||
|
||||
const FeedArticleCard({
|
||||
super.key,
|
||||
required this.article,
|
||||
this.onTagSelected,
|
||||
this.selectedTags = const {},
|
||||
});
|
||||
const FeedArticleCard({super.key, required this.article});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final tags = ref.watch(articleFilterProvider);
|
||||
|
||||
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 Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(feedRepositoryProvider.notifier)
|
||||
.touchArticleRead(article.id);
|
||||
|
||||
if (context.mounted) {
|
||||
await context.push(
|
||||
FeedArticleRoute(articleId: article.id).location,
|
||||
extra: article,
|
||||
);
|
||||
}
|
||||
await FeedArticleRoute(articleId: article.id).push(context);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@@ -52,17 +51,55 @@ class FeedArticleCard extends HookConsumerWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
UrlIcon(article.feedId.base, iconSize: 34.0),
|
||||
UrlIcon([
|
||||
article.icon ?? article.feedId.base,
|
||||
], iconSize: 34.0),
|
||||
const SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
article.displayTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
if (article.summaryPlain != null)
|
||||
if (titleHighlight.isNotEmpty)
|
||||
MarkdownBody(
|
||||
data: titleHighlight!,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (titleHighlight.isEmpty)
|
||||
Text(
|
||||
article.displayTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
if (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,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchSnippet.isEmpty &&
|
||||
article.summaryPlain != null)
|
||||
Text(
|
||||
article.summaryPlain!,
|
||||
style: theme.textTheme.bodySmall,
|
||||
@@ -86,15 +123,35 @@ class FeedArticleCard extends HookConsumerWidget {
|
||||
if (article.authors.isNotEmpty || article.tags.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
if (article.authors.isNotEmpty)
|
||||
AuthorsHorizontalList(authors: article.authors!),
|
||||
AuthorsHorizontalList(
|
||||
authors: article.authors!,
|
||||
selectedTags: tags,
|
||||
onTagSelected: (tagId, value) {
|
||||
if (value) {
|
||||
ref.read(articleFilterProvider.notifier).addTag(tagId);
|
||||
} else {
|
||||
ref
|
||||
.read(articleFilterProvider.notifier)
|
||||
.removeTag(tagId);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (article.tags.isNotEmpty)
|
||||
TagsHorizontalList(
|
||||
tags: article.tags!,
|
||||
selectedTags: selectedTags,
|
||||
onTagSelected: onTagSelected,
|
||||
selectedTags: tags,
|
||||
onTagSelected: (tagId, value) {
|
||||
if (value) {
|
||||
ref.read(articleFilterProvider.notifier).addTag(tagId);
|
||||
} else {
|
||||
ref
|
||||
.read(articleFilterProvider.notifier)
|
||||
.removeTag(tagId);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.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';
|
||||
@@ -26,7 +25,7 @@ class FeedCard extends HookConsumerWidget {
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await context.push(FeedArticleListRoute(feedId: feed.url).location);
|
||||
await FeedArticleListRoute(feedId: feed.url).push(context);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@@ -36,7 +35,9 @@ class FeedCard extends HookConsumerWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
UrlIcon(feed.url.base, iconSize: 34.0),
|
||||
UrlIcon([
|
||||
feed.icon ?? feed.siteLink ?? feed.url.base,
|
||||
], iconSize: 34.0),
|
||||
const SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -56,6 +57,12 @@ class FeedCard extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await FeedEditRoute(feedId: feed.url).push(context);
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (feed.authors.isNotEmpty || feed.tags.isNotEmpty) ...[
|
||||
@@ -89,6 +96,7 @@ class FeedCard extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
return countAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (count) {
|
||||
if (count == null) {
|
||||
return const SizedBox();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
||||
|
||||
final _tagSplitPatter = RegExp(r'[,\s]+');
|
||||
@@ -45,9 +46,8 @@ class TagField extends HookWidget {
|
||||
TextField(
|
||||
controller: textController,
|
||||
decoration: const InputDecoration(
|
||||
label: Text('Add'),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
hintText: 'tag1, tag2, ...',
|
||||
prefixIcon: Icon(MdiIcons.tagMultiple),
|
||||
),
|
||||
onChanged: (String value) {
|
||||
if (value.isNotEmpty) {
|
||||
|
||||
@@ -12,25 +12,27 @@ class TagsHorizontalList extends StatelessWidget {
|
||||
void Function(String tagId, bool value)? onTagSelected,
|
||||
}) {
|
||||
_tags =
|
||||
tags
|
||||
.map(
|
||||
(tag) => Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: FilterChip(
|
||||
label: Text(
|
||||
'${tag.id} ${tag.title.mapNotNull((title) => '($title)') ?? ''}'
|
||||
.trim(),
|
||||
),
|
||||
selected: selectedTags.contains(tag.id),
|
||||
onSelected: onTagSelected.mapNotNull(
|
||||
(onTagSelected) => (value) {
|
||||
tags.map((tag) {
|
||||
final label = Text(
|
||||
'${tag.id} ${tag.title.mapNotNull((title) => '($title)') ?? ''}'
|
||||
.trim(),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child:
|
||||
onTagSelected.mapNotNull(
|
||||
(onTagSelected) => FilterChip(
|
||||
label: label,
|
||||
selected: selectedTags.contains(tag.id),
|
||||
onSelected: (value) {
|
||||
onTagSelected(tag.id, value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
) ??
|
||||
Chip(label: label),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -43,7 +45,8 @@ class TagsHorizontalList extends StatelessWidget {
|
||||
return ListView.builder(
|
||||
itemCount: _tags.length,
|
||||
controller: controller,
|
||||
shrinkWrap: true,
|
||||
//Improve list performance by not rendering outside screen at all
|
||||
cacheExtent: 0,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => _tags[index],
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ class FeedFinder {
|
||||
// return results;
|
||||
// }
|
||||
|
||||
void _parseBody(Set<String> candidates) {
|
||||
void _parseBody(Set<Uri> candidates) {
|
||||
for (final a in document.querySelectorAll('a')) {
|
||||
var href = a.attributes['href'];
|
||||
if (href != null) {
|
||||
@@ -46,13 +46,15 @@ class FeedFinder {
|
||||
// Fix naked URLs
|
||||
href = !href.startsWith('http') ? '$_base/$href' : href;
|
||||
|
||||
candidates.add(href);
|
||||
if (Uri.tryParse(href) case final Uri uri) {
|
||||
candidates.add(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _parseHead(Set<String> candidates) {
|
||||
void _parseHead(Set<Uri> candidates) {
|
||||
for (final link in document.querySelectorAll("link[rel='alternate']")) {
|
||||
final type = link.attributes['type'];
|
||||
if (type != null) {
|
||||
@@ -61,19 +63,22 @@ class FeedFinder {
|
||||
if (href != null) {
|
||||
// Fix relative URLs
|
||||
href = href.startsWith('/') ? _base + href : href;
|
||||
candidates.add(href);
|
||||
|
||||
if (Uri.tryParse(href) case final Uri uri) {
|
||||
candidates.add(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<String>> parse({
|
||||
Future<Set<Uri>> parse({
|
||||
bool parseHead = true,
|
||||
bool parseBody = true,
|
||||
// bool verifyCandidates = true,
|
||||
}) async {
|
||||
final candidates = <String>{};
|
||||
final candidates = <Uri>{};
|
||||
|
||||
// Look for feed candidates in head
|
||||
if (parseHead) {
|
||||
|
||||
@@ -38,6 +38,7 @@ class FeedParser {
|
||||
url: url,
|
||||
title: feed.title.whenNotEmpty ?? feed.dc?.title,
|
||||
description: feed.description.whenNotEmpty ?? feed.dc?.description,
|
||||
siteLink: feed.link.mapNotNull(Uri.tryParse),
|
||||
authors: feed.dc?.creator.whenNotEmpty.mapNotNull(
|
||||
(creator) => [FeedAuthor(name: creator)],
|
||||
),
|
||||
@@ -50,6 +51,7 @@ class FeedParser {
|
||||
url: url,
|
||||
title: feed.title.whenNotEmpty ?? feed.dc?.title,
|
||||
description: feed.description.whenNotEmpty ?? feed.dc?.description,
|
||||
siteLink: feed.link.mapNotNull(Uri.tryParse),
|
||||
authors: (feed.author.whenNotEmpty ?? feed.dc?.creator.whenNotEmpty)
|
||||
.mapNotNull((creator) => [FeedAuthor(name: creator)]),
|
||||
tags:
|
||||
@@ -62,6 +64,12 @@ class FeedParser {
|
||||
return FeedData(
|
||||
url: url,
|
||||
title: feed.title.whenNotEmpty,
|
||||
icon: feed.icon.mapNotNull(Uri.tryParse),
|
||||
siteLink:
|
||||
feed.links
|
||||
.toFeedLinks()
|
||||
.getRelation(FeedLinkRelation.alternate)
|
||||
?.uri,
|
||||
description: feed.subtitle.whenNotEmpty,
|
||||
authors: authors.isNotEmpty ? authors : null,
|
||||
tags: tags,
|
||||
@@ -76,18 +84,22 @@ class FeedParser {
|
||||
|
||||
switch (_feed) {
|
||||
case final Rss1Feed feed:
|
||||
final processedContents = await GeckoTurndownService().turndownHtml(
|
||||
feed.items.map((item) => item.content?.value ?? '').toList(),
|
||||
);
|
||||
final processedContents =
|
||||
await GeckoBrowserExtensionService.turndownHtml(
|
||||
feed.items.map((item) => item.content?.value ?? '').toList(),
|
||||
);
|
||||
|
||||
final processedSummaries = await GeckoTurndownService().turndownHtml(
|
||||
feed.items
|
||||
.map(
|
||||
(item) =>
|
||||
item.description.whenNotEmpty ?? item.dc?.description ?? '',
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
final processedSummaries =
|
||||
await GeckoBrowserExtensionService.turndownHtml(
|
||||
feed.items
|
||||
.map(
|
||||
(item) =>
|
||||
item.description.whenNotEmpty ??
|
||||
item.dc?.description ??
|
||||
'',
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
return feed.items.mapIndexed((i, item) {
|
||||
final title = item.title.whenNotEmpty ?? item.dc?.title.whenNotEmpty;
|
||||
@@ -116,18 +128,22 @@ class FeedParser {
|
||||
);
|
||||
}).toList();
|
||||
case final RssFeed feed:
|
||||
final processedContents = await GeckoTurndownService().turndownHtml(
|
||||
feed.items.map((item) => item.content?.value ?? '').toList(),
|
||||
);
|
||||
final processedContents =
|
||||
await GeckoBrowserExtensionService.turndownHtml(
|
||||
feed.items.map((item) => item.content?.value ?? '').toList(),
|
||||
);
|
||||
|
||||
final processedSummaries = await GeckoTurndownService().turndownHtml(
|
||||
feed.items
|
||||
.map(
|
||||
(item) =>
|
||||
item.description.whenNotEmpty ?? item.dc?.description ?? '',
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
final processedSummaries =
|
||||
await GeckoBrowserExtensionService.turndownHtml(
|
||||
feed.items
|
||||
.map(
|
||||
(item) =>
|
||||
item.description.whenNotEmpty ??
|
||||
item.dc?.description ??
|
||||
'',
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
return feed.items.mapIndexed((i, item) {
|
||||
final title = item.title.whenNotEmpty ?? item.dc?.title.whenNotEmpty;
|
||||
@@ -169,16 +185,18 @@ class FeedParser {
|
||||
);
|
||||
}).toList();
|
||||
case final AtomFeed feed:
|
||||
final processedContents = await GeckoTurndownService().turndownHtml(
|
||||
feed.items.map((item) => item.content ?? '').toList(),
|
||||
);
|
||||
final processedContents =
|
||||
await GeckoBrowserExtensionService.turndownHtml(
|
||||
feed.items.map((item) => item.content ?? '').toList(),
|
||||
);
|
||||
|
||||
final processedSummaries = await GeckoTurndownService().turndownHtml(
|
||||
feed.items.map((item) => item.summary ?? '').toList(),
|
||||
);
|
||||
final processedSummaries =
|
||||
await GeckoBrowserExtensionService.turndownHtml(
|
||||
feed.items.map((item) => item.summary ?? '').toList(),
|
||||
);
|
||||
|
||||
final feedLink = feed.links.toFeedLinks().firstWhereOrNull(
|
||||
(link) => link.relation == FeedLinkRelation.self,
|
||||
final feedLink = feed.links.toFeedLinks().getRelation(
|
||||
FeedLinkRelation.self,
|
||||
);
|
||||
|
||||
return feed.items.mapIndexed((i, item) {
|
||||
@@ -187,9 +205,7 @@ class FeedParser {
|
||||
final tags = item.categories.toFeedCategories();
|
||||
|
||||
final itemLinks = item.links.toFeedLinks();
|
||||
final articleLink = itemLinks.firstWhereOrNull(
|
||||
(link) => link.relation == FeedLinkRelation.alternate,
|
||||
);
|
||||
final articleLink = itemLinks.getRelation(FeedLinkRelation.alternate);
|
||||
|
||||
final itemId = item.id.whenNotEmpty ?? item.title;
|
||||
final uniqueId =
|
||||
|
||||
@@ -6,10 +6,14 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'website_title.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
Future<WebPageInfo> pageInfo(Ref ref, Uri url) async {
|
||||
Future<WebPageInfo> pageInfo(
|
||||
Ref ref,
|
||||
Uri url, {
|
||||
required bool isImageRequest,
|
||||
}) async {
|
||||
final websiteService = ref.watch(genericWebsiteServiceProvider.notifier);
|
||||
|
||||
final result = await websiteService.fetchPageInfo(url);
|
||||
final result = await websiteService.fetchPageInfo(url, isImageRequest);
|
||||
|
||||
if (result.isSuccess) {
|
||||
ref.keepAlive();
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'website_title.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$pageInfoHash() => r'bdb860ec904959d7aa561045b85b75b209fb38e1';
|
||||
String _$pageInfoHash() => r'dd5644057e4ba7280275105de4788eb4046592d4';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -39,13 +39,13 @@ class PageInfoFamily extends Family<AsyncValue<WebPageInfo>> {
|
||||
const PageInfoFamily();
|
||||
|
||||
/// See also [pageInfo].
|
||||
PageInfoProvider call(Uri url) {
|
||||
return PageInfoProvider(url);
|
||||
PageInfoProvider call(Uri url, {required bool isImageRequest}) {
|
||||
return PageInfoProvider(url, isImageRequest: isImageRequest);
|
||||
}
|
||||
|
||||
@override
|
||||
PageInfoProvider getProviderOverride(covariant PageInfoProvider provider) {
|
||||
return call(provider.url);
|
||||
return call(provider.url, isImageRequest: provider.isImageRequest);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
@@ -66,9 +66,10 @@ class PageInfoFamily extends Family<AsyncValue<WebPageInfo>> {
|
||||
/// See also [pageInfo].
|
||||
class PageInfoProvider extends AutoDisposeFutureProvider<WebPageInfo> {
|
||||
/// See also [pageInfo].
|
||||
PageInfoProvider(Uri url)
|
||||
PageInfoProvider(Uri url, {required bool isImageRequest})
|
||||
: this._internal(
|
||||
(ref) => pageInfo(ref as PageInfoRef, url),
|
||||
(ref) =>
|
||||
pageInfo(ref as PageInfoRef, url, isImageRequest: isImageRequest),
|
||||
from: pageInfoProvider,
|
||||
name: r'pageInfoProvider',
|
||||
debugGetCreateSourceHash:
|
||||
@@ -78,6 +79,7 @@ class PageInfoProvider extends AutoDisposeFutureProvider<WebPageInfo> {
|
||||
dependencies: PageInfoFamily._dependencies,
|
||||
allTransitiveDependencies: PageInfoFamily._allTransitiveDependencies,
|
||||
url: url,
|
||||
isImageRequest: isImageRequest,
|
||||
);
|
||||
|
||||
PageInfoProvider._internal(
|
||||
@@ -88,9 +90,11 @@ class PageInfoProvider extends AutoDisposeFutureProvider<WebPageInfo> {
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.url,
|
||||
required this.isImageRequest,
|
||||
}) : super.internal();
|
||||
|
||||
final Uri url;
|
||||
final bool isImageRequest;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
@@ -106,6 +110,7 @@ class PageInfoProvider extends AutoDisposeFutureProvider<WebPageInfo> {
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
url: url,
|
||||
isImageRequest: isImageRequest,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -117,13 +122,16 @@ class PageInfoProvider extends AutoDisposeFutureProvider<WebPageInfo> {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PageInfoProvider && other.url == url;
|
||||
return other is PageInfoProvider &&
|
||||
other.url == url &&
|
||||
other.isImageRequest == isImageRequest;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, url.hashCode);
|
||||
hash = _SystemHash.combine(hash, isImageRequest.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
@@ -134,6 +142,9 @@ class PageInfoProvider extends AutoDisposeFutureProvider<WebPageInfo> {
|
||||
mixin PageInfoRef on AutoDisposeFutureProviderRef<WebPageInfo> {
|
||||
/// The parameter `url` of this provider.
|
||||
Uri get url;
|
||||
|
||||
/// The parameter `isImageRequest` of this provider.
|
||||
bool get isImageRequest;
|
||||
}
|
||||
|
||||
class _PageInfoProviderElement
|
||||
@@ -143,6 +154,8 @@ class _PageInfoProviderElement
|
||||
|
||||
@override
|
||||
Uri get url => (origin as PageInfoProvider).url;
|
||||
@override
|
||||
bool get isImageRequest => (origin as PageInfoProvider).isImageRequest;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
@@ -64,7 +64,6 @@ class SelectableChips<T extends S, S, K> extends StatelessWidget {
|
||||
return ListView.builder(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
|
||||
@@ -7,17 +7,17 @@ import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class UrlIcon extends HookConsumerWidget {
|
||||
final double iconSize;
|
||||
final Uri url;
|
||||
final List<Uri> urlList;
|
||||
|
||||
const UrlIcon(this.url, {required this.iconSize, super.key});
|
||||
const UrlIcon(this.urlList, {required this.iconSize, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final icon = useCachedFuture(
|
||||
() =>
|
||||
// ignore: discarded_futures
|
||||
ref.read(genericWebsiteServiceProvider.notifier).getUrlIcon(url),
|
||||
[url],
|
||||
ref.read(genericWebsiteServiceProvider.notifier).getUrlIcon(urlList),
|
||||
[urlList],
|
||||
);
|
||||
|
||||
return Skeletonizer(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
import 'package:lensai/data/models/web_page_info.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/presentation/controllers/website_title.dart';
|
||||
@@ -17,11 +20,12 @@ class WebsiteFeedTile extends HookConsumerWidget {
|
||||
final pageInfoAsync =
|
||||
(precachedInfo?.feeds != null)
|
||||
? AsyncValue.data(precachedInfo!)
|
||||
: ref.watch(pageInfoProvider(url));
|
||||
: ref.watch(pageInfoProvider(url, isImageRequest: false));
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: pageInfoAsync.isLoading && precachedInfo?.feeds == null,
|
||||
child: pageInfoAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (info) {
|
||||
if (info.feeds.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
@@ -39,6 +43,13 @@ class WebsiteFeedTile extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
await SelectFeedDialogRoute(
|
||||
feedsJson: jsonEncode(
|
||||
info.feeds!.map((feed) => feed.toString()).toList(),
|
||||
),
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
|
||||
@@ -16,11 +16,12 @@ class WebsiteTitleTile extends HookConsumerWidget {
|
||||
final pageInfoAsync =
|
||||
(precachedInfo?.isPageInfoComplete ?? false)
|
||||
? AsyncValue.data(precachedInfo!)
|
||||
: ref.watch(pageInfoProvider(url));
|
||||
: ref.watch(pageInfoProvider(url, isImageRequest: false));
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: pageInfoAsync.isLoading && precachedInfo == null,
|
||||
child: pageInfoAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (info) {
|
||||
return ListTile(
|
||||
leading: RawImage(
|
||||
@@ -36,7 +37,8 @@ class WebsiteTitleTile extends HookConsumerWidget {
|
||||
error: (error, stackTrace) {
|
||||
return FailureWidget(
|
||||
title: error.toString(),
|
||||
onRetry: () => ref.refresh(pageInfoProvider(url)),
|
||||
onRetry:
|
||||
() => ref.refresh(pageInfoProvider(url, isImageRequest: false)),
|
||||
);
|
||||
},
|
||||
loading:
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
||||
|
||||
String? validateUrl(
|
||||
String? value, {
|
||||
bool requireAuthority = true,
|
||||
bool eagerParsing = true,
|
||||
bool onlyHttpProtocol = false,
|
||||
bool required = true,
|
||||
}) {
|
||||
if (value.isEmpty) {
|
||||
if (required) {
|
||||
return 'URL must be provided';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (uri_parser.tryParseUrl(value, eagerParsing: eagerParsing)
|
||||
case final Uri url) {
|
||||
if (!requireAuthority || url.authority.isNotEmpty) {
|
||||
if (!onlyHttpProtocol ||
|
||||
(url.isScheme('https') || url.isScheme('http'))) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Inavlid URL';
|
||||
}
|
||||
@@ -23,6 +23,10 @@ class LRUCache<K, V> {
|
||||
_capacity = capacity;
|
||||
}
|
||||
|
||||
bool contains(K key) {
|
||||
return _cache.containsKey(key);
|
||||
}
|
||||
|
||||
V? get(K key) {
|
||||
final value = _cache.remove(key); // Temporarily remove the item.
|
||||
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
const FEED_MIME_TYPES = [
|
||||
"application/atom",
|
||||
"application/rss"
|
||||
];
|
||||
|
||||
// Listen for any webRequest that might be a feed
|
||||
browser.webRequest.onHeadersReceived.addListener(
|
||||
function (details) {
|
||||
let isFeed = false;
|
||||
|
||||
for (let header of details.responseHeaders) {
|
||||
if (header.name.toLowerCase() === "content-type") {
|
||||
const contentType = header.value.toLowerCase();
|
||||
|
||||
for (const mimeType of FEED_MIME_TYPES) {
|
||||
if (contentType.includes(mimeType)) {
|
||||
isFeed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFeed) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFeed) {
|
||||
port.postMessage({
|
||||
"type": "feedRequest",
|
||||
"url": details.url
|
||||
});
|
||||
|
||||
return { cancel: true };
|
||||
}
|
||||
|
||||
// Not a feed, let the browser handle it normally
|
||||
return { responseHeaders: details.responseHeaders };
|
||||
},
|
||||
{ urls: ["<all_urls>"] },
|
||||
["blocking", "responseHeaders"]
|
||||
);
|
||||
+7
-3
@@ -2,20 +2,24 @@
|
||||
"manifest_version": 2,
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "turndown@movenext.me"
|
||||
"id": "browser_extension@movenext.me"
|
||||
}
|
||||
},
|
||||
"name": "Converts html to markdown",
|
||||
"name": "Misc extensions",
|
||||
"version": "1.0",
|
||||
"background": {
|
||||
"scripts": [
|
||||
"readability.min.js",
|
||||
"background.js"
|
||||
"port.js",
|
||||
"turndown.js",
|
||||
"feed.js"
|
||||
]
|
||||
},
|
||||
"permissions": [
|
||||
"geckoViewAddons",
|
||||
"nativeMessaging",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
const port = browser.runtime.connectNative("mozacBrowserExtension");
|
||||
+1
-1
@@ -1,4 +1,3 @@
|
||||
const port = browser.runtime.connectNative("mozacTurndownHtml");
|
||||
const parser = new DOMParser();
|
||||
|
||||
port.onMessage.addListener(message => {
|
||||
@@ -15,6 +14,7 @@ port.onMessage.addListener(message => {
|
||||
});
|
||||
|
||||
port.postMessage({
|
||||
"type": "turndown",
|
||||
"id": requestId,
|
||||
"status": "success",
|
||||
"result": results
|
||||
+5
-3
@@ -8,6 +8,7 @@ import eu.lensai.flutter_mozilla_components.components.Features
|
||||
import eu.lensai.flutter_mozilla_components.components.Search
|
||||
import eu.lensai.flutter_mozilla_components.components.Services
|
||||
import eu.lensai.flutter_mozilla_components.components.UseCases
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||
@@ -21,10 +22,11 @@ class Components(private val context: Context,
|
||||
val flutterEvents: GeckoStateEvents,
|
||||
val readerViewController: ReaderViewController,
|
||||
val selectionAction: SelectionActionDelegate,
|
||||
val addonEvents: GeckoAddonEvents,
|
||||
val tabContentEvents: GeckoTabContentEvents
|
||||
private val addonEvents: GeckoAddonEvents,
|
||||
private val tabContentEvents: GeckoTabContentEvents,
|
||||
private val extensionEvents: BrowserExtensionEvents
|
||||
) {
|
||||
val core by lazy { Core(context, this, flutterEvents) }
|
||||
val core by lazy { Core(context, this, flutterEvents, extensionEvents) }
|
||||
val events by lazy { Events(flutterEvents) }
|
||||
val useCases by lazy { UseCases(context, core.engine, core.store) }
|
||||
val services by lazy { Services(context, useCases.tabsUseCases) }
|
||||
|
||||
+4
-3
@@ -8,7 +8,8 @@ import android.content.Context
|
||||
import eu.lensai.flutter_mozilla_components.feature.ContainerProxyFeature
|
||||
import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature
|
||||
import eu.lensai.flutter_mozilla_components.feature.PrefManagerFeature
|
||||
import eu.lensai.flutter_mozilla_components.feature.TurndownFeature
|
||||
import eu.lensai.flutter_mozilla_components.feature.BrowserExtensionFeature
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
||||
import mozilla.components.browser.engine.gecko.GeckoEngine
|
||||
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||
import mozilla.components.concept.engine.DefaultSettings
|
||||
@@ -44,7 +45,7 @@ object EngineProvider {
|
||||
return runtime!!
|
||||
}
|
||||
|
||||
fun createEngine(context: Context, defaultSettings: DefaultSettings): Engine {
|
||||
fun createEngine(context: Context, defaultSettings: DefaultSettings, extensionEvents: BrowserExtensionEvents): Engine {
|
||||
Logger.debug("Creating Engine")
|
||||
val runtime = getOrCreateRuntime(context)
|
||||
|
||||
@@ -53,7 +54,7 @@ object EngineProvider {
|
||||
CookieManagerFeature.install(it)
|
||||
PrefManagerFeature.install(it)
|
||||
ContainerProxyFeature.install(it)
|
||||
TurndownFeature.install(it)
|
||||
BrowserExtensionFeature.install(it, extensionEvents)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-17
@@ -2,6 +2,7 @@ package eu.lensai.flutter_mozilla_components
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoAddonsApiImpl
|
||||
@@ -18,11 +19,13 @@ import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerIm
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoSuggestionApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoTurndownApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserExtensionApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserExtensionApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController
|
||||
@@ -39,7 +42,6 @@ import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTurndownApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
|
||||
|
||||
@@ -68,7 +70,6 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
private lateinit var _flutterEvents : GeckoStateEvents
|
||||
|
||||
private var isPlatformViewRegistered = false
|
||||
private var pendingFragmentShow = false
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
synchronized(this) {
|
||||
@@ -92,6 +93,8 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
val readerViewController =
|
||||
ReaderViewController(_flutterPluginBinding.binaryMessenger)
|
||||
|
||||
val extensionEvents = BrowserExtensionEvents(_flutterPluginBinding.binaryMessenger)
|
||||
|
||||
val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger)
|
||||
val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger)
|
||||
|
||||
@@ -105,6 +108,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
selectionActionDelegate,
|
||||
addonEvents,
|
||||
tabContentEvents,
|
||||
extensionEvents
|
||||
)
|
||||
|
||||
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
|
||||
@@ -125,7 +129,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
))
|
||||
GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl())
|
||||
GeckoDownloadsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoDownloadsApiImpl())
|
||||
GeckoTurndownApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTurndownApiImpl())
|
||||
GeckoBrowserExtensionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserExtensionApiImpl())
|
||||
|
||||
ReaderViewEvents.setUp(
|
||||
_flutterPluginBinding.binaryMessenger,
|
||||
@@ -137,21 +141,31 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
flutterPluginBinding.applicationContext.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun showNativeFragment() {
|
||||
private fun showNativeFragment(): Boolean {
|
||||
if (!isPlatformViewRegistered) {
|
||||
pendingFragmentShow = true
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if (activity == null) {
|
||||
return
|
||||
if (activity == null || activity !is FragmentActivity) {
|
||||
return false
|
||||
}
|
||||
|
||||
val fragmentActivity = activity as FragmentActivity
|
||||
|
||||
// Check if the container view exists in the view hierarchy
|
||||
val container = fragmentActivity.findViewById<View>(FRAGMENT_CONTAINER_ID)
|
||||
if (container == null) {
|
||||
// Container doesn't exist yet, retry later
|
||||
return false
|
||||
}
|
||||
|
||||
val nativeFragment = BrowserFragment.create()
|
||||
val fm = (activity as FragmentActivity).supportFragmentManager
|
||||
val fm = fragmentActivity.supportFragmentManager
|
||||
fm.beginTransaction()
|
||||
.replace(FRAGMENT_CONTAINER_ID, nativeFragment)
|
||||
.commitAllowingStateLoss()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
@@ -170,12 +184,6 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
)
|
||||
|
||||
isPlatformViewRegistered = true
|
||||
|
||||
// Process any pending fragment show request
|
||||
if (pendingFragmentShow) {
|
||||
pendingFragmentShow = false
|
||||
showNativeFragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
@@ -189,6 +197,5 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
override fun onDetachedFromActivity() {
|
||||
this.activity = null
|
||||
isPlatformViewRegistered = false
|
||||
pendingFragmentShow = false
|
||||
}
|
||||
}
|
||||
|
||||
+9
-3
@@ -37,7 +37,13 @@ private class NativeFragmentView(
|
||||
FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
container = FrameLayout(activity!!)
|
||||
|
||||
// Ensure activity is not null before creating the container
|
||||
if (activity == null) {
|
||||
throw IllegalStateException("Activity cannot be null when creating NativeFragmentView")
|
||||
}
|
||||
|
||||
container = FrameLayout(activity)
|
||||
container.layoutParams = vParams
|
||||
container.id = containerId
|
||||
}
|
||||
@@ -45,8 +51,8 @@ private class NativeFragmentView(
|
||||
override fun onFlutterViewAttached(flutterView: View) {
|
||||
super.onFlutterViewAttached(flutterView)
|
||||
|
||||
components.engineReportedInitialized = false;
|
||||
flutterEvents.onViewReadyStateChange(System.currentTimeMillis(),true) { _ -> }
|
||||
components.engineReportedInitialized = false
|
||||
flutterEvents.onViewReadyStateChange(System.currentTimeMillis(), true) { _ -> }
|
||||
}
|
||||
|
||||
override fun getView(): View {
|
||||
|
||||
+5
-2
@@ -1,6 +1,7 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
|
||||
import android.content.Context
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||
@@ -44,7 +45,8 @@ object GlobalComponents {
|
||||
readerViewController: ReaderViewController,
|
||||
selectionAction: SelectionActionDelegate,
|
||||
addonEvents: GeckoAddonEvents,
|
||||
tabContentEvents: GeckoTabContentEvents
|
||||
tabContentEvents: GeckoTabContentEvents,
|
||||
extensionEvents: BrowserExtensionEvents
|
||||
) {
|
||||
Logger.debug("Creating new components")
|
||||
|
||||
@@ -54,7 +56,8 @@ object GlobalComponents {
|
||||
readerViewController,
|
||||
selectionAction,
|
||||
addonEvents,
|
||||
tabContentEvents
|
||||
tabContentEvents,
|
||||
extensionEvents
|
||||
)
|
||||
|
||||
//newComponents.crashReporter.install(applicationContext)
|
||||
|
||||
+5
-3
@@ -9,17 +9,19 @@ import mozilla.components.feature.addons.logger
|
||||
* Implementation of GeckoBrowserApi that handles browser-related operations
|
||||
* @param showFragmentCallback Callback function to show native fragment
|
||||
*/
|
||||
class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Unit) : GeckoBrowserApi {
|
||||
class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Boolean) : GeckoBrowserApi {
|
||||
companion object {
|
||||
private const val TAG = "GeckoBrowserApiImpl"
|
||||
}
|
||||
|
||||
override fun showNativeFragment() {
|
||||
override fun showNativeFragment(): Boolean {
|
||||
try {
|
||||
showFragmentCallback()
|
||||
return showFragmentCallback()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to show native fragment", e)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Long) {
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
package eu.lensai.flutter_mozilla_components.api
|
||||
|
||||
import eu.lensai.flutter_mozilla_components.feature.ResultConsumer
|
||||
import eu.lensai.flutter_mozilla_components.feature.TurndownFeature
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTurndownApi
|
||||
import eu.lensai.flutter_mozilla_components.feature.BrowserExtensionFeature
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserExtensionApi
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
class GeckoTurndownApiImpl : GeckoTurndownApi {
|
||||
class GeckoBrowserExtensionApiImpl : GeckoBrowserExtensionApi {
|
||||
private fun JSONObject.toMap(): Map<String, Any> {
|
||||
val map = mutableMapOf<String, Any>()
|
||||
val keys = this.keys()
|
||||
@@ -38,7 +38,7 @@ class GeckoTurndownApiImpl : GeckoTurndownApi {
|
||||
}
|
||||
|
||||
override fun getMarkdown(htmlList: List<String>, callback: (Result<List<Any>>) -> Unit) {
|
||||
TurndownFeature.scheduleRequest("turndown", htmlList, object :
|
||||
BrowserExtensionFeature.scheduleRequest("turndown", htmlList, object :
|
||||
ResultConsumer<JSONObject> {
|
||||
override fun success(result: JSONObject) {
|
||||
val resultArray = result.getJSONArray("result")
|
||||
+3
-1
@@ -14,6 +14,7 @@ import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
|
||||
import eu.lensai.flutter_mozilla_components.R
|
||||
import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey
|
||||
import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage
|
||||
@@ -54,6 +55,7 @@ private const val DAY_IN_MINUTES = 24 * 60L
|
||||
class Core(private val context: Context,
|
||||
private val components: Components,
|
||||
private val flutterEvents: GeckoStateEvents,
|
||||
private val extensionEvents: BrowserExtensionEvents
|
||||
) {
|
||||
val prefs by lazy {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@@ -95,7 +97,7 @@ class Core(private val context: Context,
|
||||
}
|
||||
|
||||
val engine: Engine by lazy {
|
||||
EngineProvider.createEngine(context, engineSettings)
|
||||
EngineProvider.createEngine(context, engineSettings, extensionEvents)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+49
-26
@@ -1,6 +1,7 @@
|
||||
package eu.lensai.flutter_mozilla_components.feature
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -13,13 +14,15 @@ import mozilla.components.support.base.log.logger.Logger
|
||||
import mozilla.components.support.webextensions.WebExtensionController
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||
|
||||
object TurndownFeature {
|
||||
private val logger = Logger("turndown")
|
||||
object BrowserExtensionFeature {
|
||||
private val logger = Logger("browser_extension")
|
||||
|
||||
private const val PREF_MANAGER_REPORTER_EXTENSION_ID = "turndown@movenext.me"
|
||||
private const val PREF_MANAGER_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/turndown/"
|
||||
private const val PREF_MANAGER_REPORTER_MESSAGING_ID = "mozacTurndownHtml"
|
||||
private const val PREF_MANAGER_REPORTER_EXTENSION_ID = "browser_extension@movenext.me"
|
||||
private const val PREF_MANAGER_REPORTER_EXTENSION_URL =
|
||||
"resource://android/assets/extensions/browser_extension/"
|
||||
private const val PREF_MANAGER_REPORTER_MESSAGING_ID = "mozacBrowserExtension"
|
||||
|
||||
private var nextRequestId: Int = 0
|
||||
private val requestHandlers = HashMap<Int, ResultConsumer<JSONObject>>()
|
||||
@@ -30,16 +33,22 @@ object TurndownFeature {
|
||||
internal var extensionController = WebExtensionController(
|
||||
PREF_MANAGER_REPORTER_EXTENSION_ID,
|
||||
PREF_MANAGER_REPORTER_EXTENSION_URL,
|
||||
PREF_MANAGER_REPORTER_MESSAGING_ID,
|
||||
PREF_MANAGER_REPORTER_MESSAGING_ID
|
||||
)
|
||||
|
||||
fun scheduleRequest(command: String, args: Any, callback: ResultConsumer<JSONObject>) {
|
||||
fun scheduleRequest(
|
||||
command: String,
|
||||
args: Any,
|
||||
callback: ResultConsumer<JSONObject>
|
||||
) {
|
||||
val message = JSONObject()
|
||||
message.put("action", command);
|
||||
message.put("args", when (args) {
|
||||
is List<*> -> JSONArray(args)
|
||||
else -> args
|
||||
})
|
||||
message.put(
|
||||
"args", when (args) {
|
||||
is List<*> -> JSONArray(args)
|
||||
else -> args
|
||||
}
|
||||
)
|
||||
|
||||
runBlocking {
|
||||
withContext(Dispatchers.Default) {
|
||||
@@ -56,23 +65,37 @@ object TurndownFeature {
|
||||
}
|
||||
}
|
||||
|
||||
private class TurndownBackgroundMessageHandler() : MessageHandler {
|
||||
private class ExtensionBackgroundMessageHandler(
|
||||
private val extensionEvents: BrowserExtensionEvents
|
||||
) : MessageHandler {
|
||||
override fun onPortMessage(message: Any, port: Port) {
|
||||
runBlocking {
|
||||
withContext(Dispatchers.Default) {
|
||||
mutex.withLock {
|
||||
val messageJSON = message as JSONObject;
|
||||
val type = messageJSON.getString("type")
|
||||
|
||||
val requestId = messageJSON.getInt("id")
|
||||
val status = messageJSON.getString("status")
|
||||
if (status == "success") {
|
||||
requestHandlers[requestId]?.success(message)
|
||||
} else {
|
||||
requestHandlers[requestId]?.error(
|
||||
"Pref Manager",
|
||||
"Failed to perform operation",
|
||||
message.getString("error")
|
||||
)
|
||||
if (type == "feedRequest") {
|
||||
val url = messageJSON.getString("url")
|
||||
|
||||
runOnUiThread {
|
||||
extensionEvents.onFeedRequested(
|
||||
System.currentTimeMillis(),
|
||||
url
|
||||
) { _ -> }
|
||||
}
|
||||
} else if (type == "turndown") {
|
||||
val requestId = messageJSON.getInt("id")
|
||||
val status = messageJSON.getString("status")
|
||||
if (status == "success") {
|
||||
requestHandlers[requestId]?.success(message)
|
||||
} else {
|
||||
requestHandlers[requestId]?.error(
|
||||
"Pref Manager",
|
||||
"Failed to perform operation",
|
||||
message.getString("error")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,17 +110,17 @@ object TurndownFeature {
|
||||
* @param productName a custom product name used to automatically label reports. Defaults to
|
||||
* "android-components".
|
||||
*/
|
||||
fun install(runtime: WebExtensionRuntime) {
|
||||
fun install(runtime: WebExtensionRuntime, extensionEvents: BrowserExtensionEvents) {
|
||||
extensionController.registerBackgroundMessageHandler(
|
||||
TurndownBackgroundMessageHandler(),
|
||||
ExtensionBackgroundMessageHandler(extensionEvents)
|
||||
)
|
||||
extensionController.install(
|
||||
runtime,
|
||||
onSuccess = {
|
||||
logger.debug("Installed Turndown webextension: ${it.id}")
|
||||
logger.debug("Installed browser_extension webextension: ${it.id}")
|
||||
},
|
||||
onError = { throwable ->
|
||||
logger.error("Failed to install Turndown webextension: ", throwable)
|
||||
logger.error("Failed to install browser_extension webextension: ", throwable)
|
||||
},
|
||||
)
|
||||
}
|
||||
+33
-8
@@ -1994,7 +1994,7 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface GeckoBrowserApi {
|
||||
fun showNativeFragment()
|
||||
fun showNativeFragment(): Boolean
|
||||
fun onTrimMemory(level: Long)
|
||||
|
||||
companion object {
|
||||
@@ -2011,8 +2011,7 @@ interface GeckoBrowserApi {
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
api.showNativeFragment()
|
||||
listOf(null)
|
||||
listOf(api.showNativeFragment())
|
||||
} catch (exception: Throwable) {
|
||||
wrapError(exception)
|
||||
}
|
||||
@@ -2980,20 +2979,20 @@ interface GeckoPrefApi {
|
||||
}
|
||||
}
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface GeckoTurndownApi {
|
||||
interface GeckoBrowserExtensionApi {
|
||||
fun getMarkdown(htmlList: List<String>, callback: (Result<List<Any>>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by GeckoTurndownApi. */
|
||||
/** The codec used by GeckoBrowserExtensionApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
GeckoPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `GeckoTurndownApi` to handle messages through the `binaryMessenger`. */
|
||||
/** Sets up an instance of `GeckoBrowserExtensionApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoTurndownApi?, messageChannelSuffix: String = "") {
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoBrowserExtensionApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoTurndownApi.getMarkdown$separatedMessageChannelSuffix", codec)
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserExtensionApi.getMarkdown$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
@@ -4010,3 +4009,29 @@ interface GeckoDownloadsApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
|
||||
class BrowserExtensionEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
|
||||
companion object {
|
||||
/** The codec used by BrowserExtensionEvents. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
GeckoPigeonCodec()
|
||||
}
|
||||
}
|
||||
fun onFeedRequested(timestampArg: Long, urlArg: String, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.BrowserExtensionEvents.onFeedRequested$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(timestampArg, urlArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export 'src/domain/services/gecko_session.dart';
|
||||
export 'src/domain/services/gecko_suggestions.dart';
|
||||
export 'src/domain/services/gecko_tab.dart';
|
||||
export 'src/domain/services/gecko_tab_content.dart';
|
||||
export 'src/domain/services/gecko_turndown.dart';
|
||||
export 'src/domain/services/gecko_browser_extension.dart';
|
||||
export 'src/geckoview_widget.dart';
|
||||
export 'src/pigeons/gecko.g.dart'
|
||||
show
|
||||
|
||||
@@ -7,7 +7,7 @@ class GeckoBrowserService {
|
||||
|
||||
GeckoBrowserService({GeckoBrowserApi? api}) : _api = api ?? _apiInstance;
|
||||
|
||||
Future<void> showNativeFragment() {
|
||||
Future<bool> showNativeFragment() {
|
||||
return _api.showNativeFragment();
|
||||
}
|
||||
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mozilla_components/src/domain/entities/turndown_result.dart';
|
||||
import 'package:flutter_mozilla_components/src/extensions/subject.dart';
|
||||
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
final _apiInstance = GeckoBrowserExtensionApi();
|
||||
|
||||
class GeckoBrowserExtensionService extends BrowserExtensionEvents {
|
||||
final _feedRequest = BehaviorSubject<String>();
|
||||
|
||||
Stream<String> get feedRequested => _feedRequest.stream;
|
||||
|
||||
static Future<List<TurndownResults>> turndownHtml(
|
||||
List<String> htmlList,
|
||||
) async {
|
||||
final markdownResult = await _apiInstance.getMarkdown(htmlList);
|
||||
|
||||
final results =
|
||||
markdownResult
|
||||
.cast()
|
||||
.map(
|
||||
(result) => TurndownResults(
|
||||
// ignore: avoid_dynamic_calls valid
|
||||
markdown: result['fullContentMarkdown'] as String,
|
||||
// ignore: avoid_dynamic_calls valid
|
||||
plain: result['fullContentPlain'] as String,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
GeckoBrowserExtensionService.setUp({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) {
|
||||
BrowserExtensionEvents.setUp(
|
||||
this,
|
||||
binaryMessenger: binaryMessenger,
|
||||
messageChannelSuffix: messageChannelSuffix,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onFeedRequested(int timestamp, String url) {
|
||||
_feedRequest.addWhenMoreRecent(timestamp, null, url);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
unawaited(_feedRequest.close());
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import 'package:flutter_mozilla_components/src/domain/entities/turndown_result.dart';
|
||||
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
|
||||
|
||||
final _apiInstance = GeckoTurndownApi();
|
||||
|
||||
class GeckoTurndownService {
|
||||
Future<List<TurndownResults>> turndownHtml(List<String> htmlList) async {
|
||||
final markdownResult = await _apiInstance.getMarkdown(htmlList);
|
||||
|
||||
final results =
|
||||
markdownResult
|
||||
.cast()
|
||||
.map(
|
||||
(result) => TurndownResults(
|
||||
// ignore: avoid_dynamic_calls valid
|
||||
markdown: result['fullContentMarkdown'] as String,
|
||||
// ignore: avoid_dynamic_calls valid
|
||||
plain: result['fullContentPlain'] as String,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user