web feed intermediate

This commit is contained in:
Fabian Freund
2025-02-28 14:31:07 +01:00
parent 3c4d479bd9
commit 2a2311253b
51 changed files with 5503 additions and 0 deletions
@@ -0,0 +1,38 @@
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/domain/repositories/feed_repository.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'providers.g.dart';
@Riverpod()
Stream<List<FeedData>> feedList(Ref ref) {
final repository = ref.watch(feedRepositoryProvider.notifier);
return repository.watchFeeds();
}
@Riverpod()
Stream<List<FeedArticle>> feedArticleList(Ref ref, FeedFilter filter) {
final repository = ref.watch(feedRepositoryProvider.notifier);
return repository.watchFeedArticles(filter);
}
@Riverpod()
Stream<FeedArticle?> feedArticle(Ref ref, String articleId) {
final repository = ref.watch(feedRepositoryProvider.notifier);
return 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);
return stream.map((counts) => counts[feedId.toString()]);
}
@@ -0,0 +1,433 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$feedListHash() => r'0076186437354768c39fb1d7c8bcfcf7b94c7dd1';
/// See also [feedList].
@ProviderFor(feedList)
final feedListProvider = AutoDisposeStreamProvider<List<FeedData>>.internal(
feedList,
name: r'feedListProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$feedListHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef FeedListRef = AutoDisposeStreamProviderRef<List<FeedData>>;
String _$feedArticleListHash() => r'64e834a1b69d913f1c860b38956b69af9e89a833';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [feedArticleList].
@ProviderFor(feedArticleList)
const feedArticleListProvider = FeedArticleListFamily();
/// See also [feedArticleList].
class FeedArticleListFamily extends Family<AsyncValue<List<FeedArticle>>> {
/// See also [feedArticleList].
const FeedArticleListFamily();
/// See also [feedArticleList].
FeedArticleListProvider call(FeedFilter filter) {
return FeedArticleListProvider(filter);
}
@override
FeedArticleListProvider getProviderOverride(
covariant FeedArticleListProvider provider,
) {
return call(provider.filter);
}
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'feedArticleListProvider';
}
/// See also [feedArticleList].
class FeedArticleListProvider
extends AutoDisposeStreamProvider<List<FeedArticle>> {
/// See also [feedArticleList].
FeedArticleListProvider(FeedFilter filter)
: this._internal(
(ref) => feedArticleList(ref as FeedArticleListRef, filter),
from: feedArticleListProvider,
name: r'feedArticleListProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$feedArticleListHash,
dependencies: FeedArticleListFamily._dependencies,
allTransitiveDependencies:
FeedArticleListFamily._allTransitiveDependencies,
filter: filter,
);
FeedArticleListProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.filter,
}) : super.internal();
final FeedFilter filter;
@override
Override overrideWith(
Stream<List<FeedArticle>> Function(FeedArticleListRef provider) create,
) {
return ProviderOverride(
origin: this,
override: FeedArticleListProvider._internal(
(ref) => create(ref as FeedArticleListRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
filter: filter,
),
);
}
@override
AutoDisposeStreamProviderElement<List<FeedArticle>> createElement() {
return _FeedArticleListProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is FeedArticleListProvider && other.filter == filter;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, filter.hashCode);
return _SystemHash.finish(hash);
}
}
@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;
}
class _FeedArticleListProviderElement
extends AutoDisposeStreamProviderElement<List<FeedArticle>>
with FeedArticleListRef {
_FeedArticleListProviderElement(super.provider);
@override
FeedFilter get filter => (origin as FeedArticleListProvider).filter;
}
String _$feedArticleHash() => r'b1670f2ce11636f42fc0595befe42ada19319880';
/// See also [feedArticle].
@ProviderFor(feedArticle)
const feedArticleProvider = FeedArticleFamily();
/// See also [feedArticle].
class FeedArticleFamily extends Family<AsyncValue<FeedArticle?>> {
/// See also [feedArticle].
const FeedArticleFamily();
/// See also [feedArticle].
FeedArticleProvider call(String articleId) {
return FeedArticleProvider(articleId);
}
@override
FeedArticleProvider getProviderOverride(
covariant FeedArticleProvider provider,
) {
return call(provider.articleId);
}
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'feedArticleProvider';
}
/// See also [feedArticle].
class FeedArticleProvider extends AutoDisposeStreamProvider<FeedArticle?> {
/// See also [feedArticle].
FeedArticleProvider(String articleId)
: this._internal(
(ref) => feedArticle(ref as FeedArticleRef, articleId),
from: feedArticleProvider,
name: r'feedArticleProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$feedArticleHash,
dependencies: FeedArticleFamily._dependencies,
allTransitiveDependencies: FeedArticleFamily._allTransitiveDependencies,
articleId: articleId,
);
FeedArticleProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.articleId,
}) : super.internal();
final String articleId;
@override
Override overrideWith(
Stream<FeedArticle?> Function(FeedArticleRef provider) create,
) {
return ProviderOverride(
origin: this,
override: FeedArticleProvider._internal(
(ref) => create(ref as FeedArticleRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
articleId: articleId,
),
);
}
@override
AutoDisposeStreamProviderElement<FeedArticle?> createElement() {
return _FeedArticleProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is FeedArticleProvider && other.articleId == articleId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, articleId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin FeedArticleRef on AutoDisposeStreamProviderRef<FeedArticle?> {
/// The parameter `articleId` of this provider.
String get articleId;
}
class _FeedArticleProviderElement
extends AutoDisposeStreamProviderElement<FeedArticle?>
with FeedArticleRef {
_FeedArticleProviderElement(super.provider);
@override
String get articleId => (origin as FeedArticleProvider).articleId;
}
String _$unreadArticleCountHash() =>
r'6fb96215fb3b7739a6cbd594a0358415ced04fca';
/// See also [_unreadArticleCount].
@ProviderFor(_unreadArticleCount)
final _unreadArticleCountProvider =
AutoDisposeProvider<Raw<Stream<Map<String, int>>>>.internal(
_unreadArticleCount,
name: r'_unreadArticleCountProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$unreadArticleCountHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef _UnreadArticleCountRef =
AutoDisposeProviderRef<Raw<Stream<Map<String, int>>>>;
String _$unreadFeedArticleCountHash() =>
r'f8573674477f0d9813b42c82d75e8196c1c1445b';
/// See also [unreadFeedArticleCount].
@ProviderFor(unreadFeedArticleCount)
const unreadFeedArticleCountProvider = UnreadFeedArticleCountFamily();
/// See also [unreadFeedArticleCount].
class UnreadFeedArticleCountFamily extends Family<AsyncValue<int?>> {
/// See also [unreadFeedArticleCount].
const UnreadFeedArticleCountFamily();
/// See also [unreadFeedArticleCount].
UnreadFeedArticleCountProvider call(Uri feedId) {
return UnreadFeedArticleCountProvider(feedId);
}
@override
UnreadFeedArticleCountProvider getProviderOverride(
covariant UnreadFeedArticleCountProvider 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'unreadFeedArticleCountProvider';
}
/// See also [unreadFeedArticleCount].
class UnreadFeedArticleCountProvider extends AutoDisposeStreamProvider<int?> {
/// See also [unreadFeedArticleCount].
UnreadFeedArticleCountProvider(Uri feedId)
: this._internal(
(ref) =>
unreadFeedArticleCount(ref as UnreadFeedArticleCountRef, feedId),
from: unreadFeedArticleCountProvider,
name: r'unreadFeedArticleCountProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$unreadFeedArticleCountHash,
dependencies: UnreadFeedArticleCountFamily._dependencies,
allTransitiveDependencies:
UnreadFeedArticleCountFamily._allTransitiveDependencies,
feedId: feedId,
);
UnreadFeedArticleCountProvider._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<int?> Function(UnreadFeedArticleCountRef provider) create,
) {
return ProviderOverride(
origin: this,
override: UnreadFeedArticleCountProvider._internal(
(ref) => create(ref as UnreadFeedArticleCountRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
feedId: feedId,
),
);
}
@override
AutoDisposeStreamProviderElement<int?> createElement() {
return _UnreadFeedArticleCountProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is UnreadFeedArticleCountProvider && 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 UnreadFeedArticleCountRef on AutoDisposeStreamProviderRef<int?> {
/// The parameter `feedId` of this provider.
Uri get feedId;
}
class _UnreadFeedArticleCountProviderElement
extends AutoDisposeStreamProviderElement<int?>
with UnreadFeedArticleCountRef {
_UnreadFeedArticleCountProviderElement(super.provider);
@override
Uri get feedId => (origin as UnreadFeedArticleCountProvider).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
@@ -0,0 +1,26 @@
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';
@Riverpod(keepAlive: true)
class ArticleFilter extends _$ArticleFilter {
void addTag(String tagId) {
final tags = {...?state.tags, tagId};
state = state.copyWith.tags(tags);
}
void removeTag(String tagId) {
if (state.tags.isNotEmpty) {
final tags = {...state.tags!}..remove(tagId);
state = state.copyWith.tags(tags);
}
}
@override
FeedFilter build() {
return FeedFilter();
}
}
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'article_filter.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$articleFilterHash() => r'dfc997af8a33cbcb995288ef633ff321e95bae8d';
/// See also [ArticleFilter].
@ProviderFor(ArticleFilter)
final articleFilterProvider =
NotifierProvider<ArticleFilter, FeedFilter>.internal(
ArticleFilter.new,
name: r'articleFilterProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$articleFilterHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ArticleFilter = Notifier<FeedFilter>;
// 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
@@ -0,0 +1,117 @@
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';
part 'feed_repository.g.dart';
@Riverpod(keepAlive: true)
class FeedRepository extends _$FeedRepository {
Future<List<FeedData>> getAllFeeds() {
return ref.read(feedDatabaseProvider).feedDao.getFeeds().get();
}
Future<void> touchFeedFetched(Uri url) {
return ref
.read(feedDatabaseProvider)
.feedDao
.updateFeedFetched(url, DateTime.now());
}
Future<void> upsertFeed(FeedData feedData) {
return ref.read(feedDatabaseProvider).feedDao.upsertFeed(feedData);
}
Future<void> upsertArticles(List<FeedArticle> articles) {
return ref.read(feedDatabaseProvider).articleDao.upsertArticles(articles);
}
Future<int> deleteFeed(Uri url) {
return ref.read(feedDatabaseProvider).feedDao.deleteFeed(url);
}
Future<void> touchArticleRead(String articleId) {
return ref
.read(feedDatabaseProvider)
.articleDao
.updateArticleRead(articleId, DateTime.now());
}
Future<void> unsetArticleRead(String articleId) {
return ref
.read(feedDatabaseProvider)
.articleDao
.updateArticleRead(articleId, null);
}
Stream<List<FeedData>> watchFeeds() {
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();
if (filter.tags.isNotEmpty) {
return stream.map(
(articles) =>
articles
.where(
(article) =>
article.tags?.toSet().containsAll(filter.tags!) ?? false,
)
.toList(),
);
} else {
return stream;
}
}
Stream<FeedArticle?> watchArticle(String articleId) {
return ref
.read(feedDatabaseProvider)
.articleDao
.getArticleById(articleId)
.watchSingleOrNull();
}
Stream<Map<String, int>> watchUnreadFeedArticleCount() {
return ref
.read(feedDatabaseProvider)
.articleDao
.getUnreadArticleCount()
.watch()
.map(
(results) =>
Map.fromEntries(results.map((e) => MapEntry(e.$1, e.$2))),
);
}
@override
void build() {}
}
@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_repository.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$feedRepositoryHash() => r'6a050933a6a4eafbe76a66262e1b64bd838a1078';
/// See also [FeedRepository].
@ProviderFor(FeedRepository)
final feedRepositoryProvider = NotifierProvider<FeedRepository, void>.internal(
FeedRepository.new,
name: r'feedRepositoryProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$feedRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$FeedRepository = Notifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -0,0 +1,45 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:lensai/features/web_feed/data/models/feed_parse_result.dart';
import 'package:lensai/features/web_feed/utils/feed_parser.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'feed_reader.g.dart';
@Riverpod(keepAlive: true)
class FeedReader extends _$FeedReader {
Future<FeedParseResult> parseFeed(Uri url) async {
final rootIsolateToken = ServicesBinding.rootIsolateToken!;
final result = await compute((args) async {
// Initialize BackgroundIsolateBinaryMessenger with the token
BackgroundIsolateBinaryMessenger.ensureInitialized(
args['token']! as RootIsolateToken,
);
final client = http.Client();
try {
final url = Uri.parse(args['url']! as String);
final response = await client
.get(url)
.timeout(const Duration(seconds: 30));
final parser = FeedParser.parse(url: url, xmlString: response.body);
final result = FeedParseResult(
feedData: parser.readGeneralData(),
articleData: await parser.readArticles(),
);
return result.toJson();
} finally {
client.close();
}
}, {'token': rootIsolateToken, 'url': url.toString()});
return FeedParseResult.fromJson(result);
}
@override
void build() {}
}
@@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_reader.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$feedReaderHash() => r'67b09485e40c522108709ca32b9c624b4db2446b';
/// See also [FeedReader].
@ProviderFor(FeedReader)
final feedReaderProvider = NotifierProvider<FeedReader, void>.internal(
FeedReader.new,
name: r'feedReaderProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$feedReaderHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$FeedReader = Notifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package