prepare for multiple apps

This commit is contained in:
Fabian Freund
2026-04-06 12:23:11 +02:00
parent bd1600e8dc
commit 5afc323f04
904 changed files with 29 additions and 29 deletions
@@ -0,0 +1,184 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'package:nullability/nullability.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
import 'package:weblibre/features/web_feed/data/database/definitions.drift.dart';
import 'package:weblibre/features/web_feed/data/models/feed_article.dart';
import 'package:weblibre/features/web_feed/data/models/feed_parse_result.dart';
import 'package:weblibre/features/web_feed/data/providers.dart';
import 'package:weblibre/features/web_feed/domain/providers/article_filter.dart';
import 'package:weblibre/features/web_feed/domain/repositories/feed_repository.dart';
import 'package:weblibre/features/web_feed/domain/services/feed_reader.dart';
part 'providers.g.dart';
@Riverpod()
class ArticleSearch extends _$ArticleSearch {
late StreamController<List<FeedArticle>> _streamController;
Future<void> search(
String input, {
int snippetLength = 120,
int maxResults = 25,
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,
limit: maxResults,
)
.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 ConcatStream([Stream.value([]), _streamController.stream]);
}
}
@Riverpod()
Stream<List<FeedData>> feedList(Ref ref) {
final repository = ref.watch(feedRepositoryProvider.notifier);
return repository.watchFeeds();
}
@Riverpod()
Stream<FeedData?> feedData(Ref ref, Uri? feedId) {
final repository = ref.watch(feedRepositoryProvider.notifier);
if (feedId == null) {
return Stream.value(null);
}
return repository.watchFeed(feedId);
}
@Riverpod()
Stream<List<FeedArticle>> feedArticleList(Ref ref, Uri? feedId) {
final repository = ref.watch(feedRepositoryProvider.notifier);
return repository.watchFeedArticles(feedId);
}
@Riverpod()
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);
return stream.map((counts) => counts[feedId.toString()]);
}
@Riverpod()
Future<FeedParseResult> fetchWebFeed(Ref ref, Uri url) {
return ref.read(feedReaderProvider.notifier).parseFeed(url);
}
@@ -0,0 +1,678 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ArticleSearch)
final articleSearchProvider = ArticleSearchFamily._();
final class ArticleSearchProvider
extends $StreamNotifierProvider<ArticleSearch, List<FeedArticle>> {
ArticleSearchProvider._({
required ArticleSearchFamily super.from,
required Uri? super.argument,
}) : super(
retry: null,
name: r'articleSearchProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$articleSearchHash();
@override
String toString() {
return r'articleSearchProvider'
''
'($argument)';
}
@$internal
@override
ArticleSearch create() => ArticleSearch();
@override
bool operator ==(Object other) {
return other is ArticleSearchProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$articleSearchHash() => r'48ed3baa560c0e731626f79a8f6ff3dab1e9bc95';
final class ArticleSearchFamily extends $Family
with
$ClassFamilyOverride<
ArticleSearch,
AsyncValue<List<FeedArticle>>,
List<FeedArticle>,
Stream<List<FeedArticle>>,
Uri?
> {
ArticleSearchFamily._()
: super(
retry: null,
name: r'articleSearchProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ArticleSearchProvider call(Uri? feedId) =>
ArticleSearchProvider._(argument: feedId, from: this);
@override
String toString() => r'articleSearchProvider';
}
abstract class _$ArticleSearch extends $StreamNotifier<List<FeedArticle>> {
late final _$args = ref.$arg as Uri?;
Uri? get feedId => _$args;
Stream<List<FeedArticle>> build(Uri? feedId);
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref as $Ref<AsyncValue<List<FeedArticle>>, List<FeedArticle>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<FeedArticle>>, List<FeedArticle>>,
AsyncValue<List<FeedArticle>>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@ProviderFor(feedList)
final feedListProvider = FeedListProvider._();
final class FeedListProvider
extends
$FunctionalProvider<
AsyncValue<List<FeedData>>,
List<FeedData>,
Stream<List<FeedData>>
>
with $FutureModifier<List<FeedData>>, $StreamProvider<List<FeedData>> {
FeedListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'feedListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$feedListHash();
@$internal
@override
$StreamProviderElement<List<FeedData>> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<List<FeedData>> create(Ref ref) {
return feedList(ref);
}
}
String _$feedListHash() => r'0076186437354768c39fb1d7c8bcfcf7b94c7dd1';
@ProviderFor(feedData)
final feedDataProvider = FeedDataFamily._();
final class FeedDataProvider
extends
$FunctionalProvider<AsyncValue<FeedData?>, FeedData?, Stream<FeedData?>>
with $FutureModifier<FeedData?>, $StreamProvider<FeedData?> {
FeedDataProvider._({
required FeedDataFamily super.from,
required Uri? super.argument,
}) : super(
retry: null,
name: r'feedDataProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$feedDataHash();
@override
String toString() {
return r'feedDataProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<FeedData?> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<FeedData?> create(Ref ref) {
final argument = this.argument as Uri?;
return feedData(ref, argument);
}
@override
bool operator ==(Object other) {
return other is FeedDataProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$feedDataHash() => r'0599a2e3d159ef3abb6c3d2c871f87da2f5e646b';
final class FeedDataFamily extends $Family
with $FunctionalFamilyOverride<Stream<FeedData?>, Uri?> {
FeedDataFamily._()
: super(
retry: null,
name: r'feedDataProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
FeedDataProvider call(Uri? feedId) =>
FeedDataProvider._(argument: feedId, from: this);
@override
String toString() => r'feedDataProvider';
}
@ProviderFor(feedArticleList)
final feedArticleListProvider = FeedArticleListFamily._();
final class FeedArticleListProvider
extends
$FunctionalProvider<
AsyncValue<List<FeedArticle>>,
List<FeedArticle>,
Stream<List<FeedArticle>>
>
with
$FutureModifier<List<FeedArticle>>,
$StreamProvider<List<FeedArticle>> {
FeedArticleListProvider._({
required FeedArticleListFamily super.from,
required Uri? super.argument,
}) : super(
retry: null,
name: r'feedArticleListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$feedArticleListHash();
@override
String toString() {
return r'feedArticleListProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<List<FeedArticle>> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<List<FeedArticle>> create(Ref ref) {
final argument = this.argument as Uri?;
return feedArticleList(ref, argument);
}
@override
bool operator ==(Object other) {
return other is FeedArticleListProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$feedArticleListHash() => r'45d585cc9f59ad48a0d1d6fbcf802b1c7de7f6bc';
final class FeedArticleListFamily extends $Family
with $FunctionalFamilyOverride<Stream<List<FeedArticle>>, Uri?> {
FeedArticleListFamily._()
: super(
retry: null,
name: r'feedArticleListProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
FeedArticleListProvider call(Uri? feedId) =>
FeedArticleListProvider._(argument: feedId, from: this);
@override
String toString() => r'feedArticleListProvider';
}
@ProviderFor(FilteredArticleList)
final filteredArticleListProvider = FilteredArticleListFamily._();
final class FilteredArticleListProvider
extends
$NotifierProvider<FilteredArticleList, AsyncValue<List<FeedArticle>>> {
FilteredArticleListProvider._({
required FilteredArticleListFamily super.from,
required Uri? super.argument,
}) : super(
retry: null,
name: r'filteredArticleListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$filteredArticleListHash();
@override
String toString() {
return r'filteredArticleListProvider'
''
'($argument)';
}
@$internal
@override
FilteredArticleList create() => FilteredArticleList();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AsyncValue<List<FeedArticle>> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AsyncValue<List<FeedArticle>>>(
value,
),
);
}
@override
bool operator ==(Object other) {
return other is FilteredArticleListProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$filteredArticleListHash() =>
r'a691c3c6aa722dd85ee4980e6c48721a8025f1d8';
final class FilteredArticleListFamily extends $Family
with
$ClassFamilyOverride<
FilteredArticleList,
AsyncValue<List<FeedArticle>>,
AsyncValue<List<FeedArticle>>,
AsyncValue<List<FeedArticle>>,
Uri?
> {
FilteredArticleListFamily._()
: super(
retry: null,
name: r'filteredArticleListProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
FilteredArticleListProvider call(Uri? feedId) =>
FilteredArticleListProvider._(argument: feedId, from: this);
@override
String toString() => r'filteredArticleListProvider';
}
abstract class _$FilteredArticleList
extends $Notifier<AsyncValue<List<FeedArticle>>> {
late final _$args = ref.$arg as Uri?;
Uri? get feedId => _$args;
AsyncValue<List<FeedArticle>> build(Uri? feedId);
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<
AsyncValue<List<FeedArticle>>,
AsyncValue<List<FeedArticle>>
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<List<FeedArticle>>,
AsyncValue<List<FeedArticle>>
>,
AsyncValue<List<FeedArticle>>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@ProviderFor(feedArticle)
final feedArticleProvider = FeedArticleFamily._();
final class FeedArticleProvider
extends
$FunctionalProvider<
AsyncValue<FeedArticle?>,
FeedArticle?,
Stream<FeedArticle?>
>
with $FutureModifier<FeedArticle?>, $StreamProvider<FeedArticle?> {
FeedArticleProvider._({
required FeedArticleFamily super.from,
required (String, {bool updateReadDate}) super.argument,
}) : super(
retry: null,
name: r'feedArticleProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$feedArticleHash();
@override
String toString() {
return r'feedArticleProvider'
''
'$argument';
}
@$internal
@override
$StreamProviderElement<FeedArticle?> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<FeedArticle?> create(Ref ref) {
final argument = this.argument as (String, {bool updateReadDate});
return feedArticle(
ref,
argument.$1,
updateReadDate: argument.updateReadDate,
);
}
@override
bool operator ==(Object other) {
return other is FeedArticleProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$feedArticleHash() => r'18b5faf391867b95f4b3bc4f74a6083854b633a5';
final class FeedArticleFamily extends $Family
with
$FunctionalFamilyOverride<
Stream<FeedArticle?>,
(String, {bool updateReadDate})
> {
FeedArticleFamily._()
: super(
retry: null,
name: r'feedArticleProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
FeedArticleProvider call(String articleId, {required bool updateReadDate}) =>
FeedArticleProvider._(
argument: (articleId, updateReadDate: updateReadDate),
from: this,
);
@override
String toString() => r'feedArticleProvider';
}
@ProviderFor(unreadArticleCount)
final unreadArticleCountProvider = UnreadArticleCountProvider._();
final class UnreadArticleCountProvider
extends
$FunctionalProvider<
Raw<Stream<Map<String, int>>>,
Raw<Stream<Map<String, int>>>,
Raw<Stream<Map<String, int>>>
>
with $Provider<Raw<Stream<Map<String, int>>>> {
UnreadArticleCountProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'unreadArticleCountProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$unreadArticleCountHash();
@$internal
@override
$ProviderElement<Raw<Stream<Map<String, int>>>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
Raw<Stream<Map<String, int>>> create(Ref ref) {
return unreadArticleCount(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Raw<Stream<Map<String, int>>> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Raw<Stream<Map<String, int>>>>(
value,
),
);
}
}
String _$unreadArticleCountHash() =>
r'709518ad229636df0f1095f47e3a6d116b3aa7e6';
@ProviderFor(unreadFeedArticleCount)
final unreadFeedArticleCountProvider = UnreadFeedArticleCountFamily._();
final class UnreadFeedArticleCountProvider
extends $FunctionalProvider<AsyncValue<int?>, int?, Stream<int?>>
with $FutureModifier<int?>, $StreamProvider<int?> {
UnreadFeedArticleCountProvider._({
required UnreadFeedArticleCountFamily super.from,
required Uri super.argument,
}) : super(
retry: null,
name: r'unreadFeedArticleCountProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$unreadFeedArticleCountHash();
@override
String toString() {
return r'unreadFeedArticleCountProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<int?> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<int?> create(Ref ref) {
final argument = this.argument as Uri;
return unreadFeedArticleCount(ref, argument);
}
@override
bool operator ==(Object other) {
return other is UnreadFeedArticleCountProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$unreadFeedArticleCountHash() =>
r'5e0d8e58b3d1dec978dc07b22367f2cb037b1b15';
final class UnreadFeedArticleCountFamily extends $Family
with $FunctionalFamilyOverride<Stream<int?>, Uri> {
UnreadFeedArticleCountFamily._()
: super(
retry: null,
name: r'unreadFeedArticleCountProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
UnreadFeedArticleCountProvider call(Uri feedId) =>
UnreadFeedArticleCountProvider._(argument: feedId, from: this);
@override
String toString() => r'unreadFeedArticleCountProvider';
}
@ProviderFor(fetchWebFeed)
final fetchWebFeedProvider = FetchWebFeedFamily._();
final class FetchWebFeedProvider
extends
$FunctionalProvider<
AsyncValue<FeedParseResult>,
FeedParseResult,
FutureOr<FeedParseResult>
>
with $FutureModifier<FeedParseResult>, $FutureProvider<FeedParseResult> {
FetchWebFeedProvider._({
required FetchWebFeedFamily super.from,
required Uri super.argument,
}) : super(
retry: null,
name: r'fetchWebFeedProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$fetchWebFeedHash();
@override
String toString() {
return r'fetchWebFeedProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<FeedParseResult> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<FeedParseResult> create(Ref ref) {
final argument = this.argument as Uri;
return fetchWebFeed(ref, argument);
}
@override
bool operator ==(Object other) {
return other is FetchWebFeedProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$fetchWebFeedHash() => r'73bdf87ad7dbd039c7dc181d80acdf99d96fe1c6';
final class FetchWebFeedFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<FeedParseResult>, Uri> {
FetchWebFeedFamily._()
: super(
retry: null,
name: r'fetchWebFeedProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
FetchWebFeedProvider call(Uri url) =>
FetchWebFeedProvider._(argument: url, from: this);
@override
String toString() => r'fetchWebFeedProvider';
}
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:nullability/nullability.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/logger.dart';
part 'add_dialog_blocking.g.dart';
@Riverpod(keepAlive: true)
class AddFeedDialogBlocking extends _$AddFeedDialogBlocking {
DateTime? _lastIgnore;
final _ignoredUrls = <Uri, DateTime>{};
void ignore(Uri url) {
final date = DateTime.now();
_lastIgnore = date;
_ignoredUrls[url] = date;
}
bool canPush(Uri url) {
if (_lastIgnore.mapNotNull(
(last) =>
DateTime.now().difference(last) <= const Duration(seconds: 30),
) ??
false) {
logger.i('Blocking add feed default timeout for $url');
return false;
}
if (_ignoredUrls[url].mapNotNull(
(last) =>
DateTime.now().difference(last) <= const Duration(minutes: 5),
) ??
false) {
logger.i('Blocking add feed url specific for $url');
return false;
}
return true;
}
@override
void build() {}
}
@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'add_dialog_blocking.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(AddFeedDialogBlocking)
final addFeedDialogBlockingProvider = AddFeedDialogBlockingProvider._();
final class AddFeedDialogBlockingProvider
extends $NotifierProvider<AddFeedDialogBlocking, void> {
AddFeedDialogBlockingProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'addFeedDialogBlockingProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$addFeedDialogBlockingHash();
@$internal
@override
AddFeedDialogBlocking create() => AddFeedDialogBlocking();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$addFeedDialogBlockingHash() =>
r'513071d5e507dd292df6b4a3ee3ef7d2217db5bb';
abstract class _$AddFeedDialogBlocking extends $Notifier<void> {
void build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'article_filter.g.dart';
@Riverpod(keepAlive: true)
class ArticleFilter extends _$ArticleFilter {
void addTag(String tagId) {
state = {...state, tagId};
}
void removeTag(String tagId) {
if (state.isNotEmpty) {
state = {...state}..remove(tagId);
}
}
@override
Set<String> build() {
return {};
}
}
@@ -0,0 +1,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'article_filter.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ArticleFilter)
final articleFilterProvider = ArticleFilterProvider._();
final class ArticleFilterProvider
extends $NotifierProvider<ArticleFilter, Set<String>> {
ArticleFilterProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'articleFilterProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$articleFilterHash();
@$internal
@override
ArticleFilter create() => ArticleFilter();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Set<String> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Set<String>>(value),
);
}
}
String _$articleFilterHash() => r'61e4d5230e214038e753bc173158ed1d4dc57040';
abstract class _$ArticleFilter extends $Notifier<Set<String>> {
Set<String> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<Set<String>, Set<String>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Set<String>, Set<String>>,
Set<String>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/web_feed/data/database/definitions.drift.dart';
import 'package:weblibre/features/web_feed/data/models/feed_article.dart';
import 'package:weblibre/features/web_feed/data/providers.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 feedId) {
return ref
.read(feedDatabaseProvider)
.feedDao
.updateFeedFetched(feedId, 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 feedId) {
return ref.read(feedDatabaseProvider).feedDao.deleteFeed(feedId);
}
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<FeedData?> watchFeed(Uri feedId) {
return ref
.read(feedDatabaseProvider)
.feedDao
.getFeed(feedId)
.watchSingleOrNull();
}
Stream<List<FeedArticle>> watchFeedArticles(Uri? feedId) {
return ref
.read(feedDatabaseProvider)
.articleDao
.getFeedArticles(feedId)
.watch();
}
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,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_repository.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(FeedRepository)
final feedRepositoryProvider = FeedRepositoryProvider._();
final class FeedRepositoryProvider
extends $NotifierProvider<FeedRepository, void> {
FeedRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'feedRepositoryProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$feedRepositoryHash();
@$internal
@override
FeedRepository create() => FeedRepository();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$feedRepositoryHash() => r'805cc26890b0d43576a1eab0ecb5851b6649d7b7';
abstract class _$FeedRepository extends $Notifier<void> {
void build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:collection/collection.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/features/web_feed/data/models/feed_article.dart';
import 'package:weblibre/features/web_feed/data/providers.dart';
part 'article_content_processor.g.dart';
@Riverpod(keepAlive: true)
class ArticleContentProcessorService extends _$ArticleContentProcessorService {
@override
void build() {
final db = ref.watch(feedDatabaseProvider);
final processSub = db.articleDao.getUnprocessedArticles().watch().listen((
articles,
) async {
try {
final content = await GeckoBrowserExtensionService.turndownHtml(
articles.map((article) => article.contentHtml ?? '').toList(),
);
final summary = await GeckoBrowserExtensionService.turndownHtml(
articles.map((article) => article.summaryHtml ?? '').toList(),
);
await db.articleDao.updateArticleContent(
articles
.mapIndexed(
(index, article) => article.copyWith(
contentMarkdown: content[index].markdown ?? '',
contentPlain: content[index].plain,
summaryMarkdown: summary[index].markdown ?? '',
summaryPlain: summary[index].plain,
),
)
.toList(),
);
logger.i('Processed ${articles.length} articles');
} catch (e, s) {
logger.e('Error processing articles', error: e, stackTrace: s);
}
});
ref.onDispose(() async {
await processSub.cancel();
});
}
}
@@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'article_content_processor.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ArticleContentProcessorService)
final articleContentProcessorServiceProvider =
ArticleContentProcessorServiceProvider._();
final class ArticleContentProcessorServiceProvider
extends $NotifierProvider<ArticleContentProcessorService, void> {
ArticleContentProcessorServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'articleContentProcessorServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$articleContentProcessorServiceHash();
@$internal
@override
ArticleContentProcessorService create() => ArticleContentProcessorService();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$articleContentProcessorServiceHash() =>
r'e7cc3da71f6dcf39b0c10df4dcd061f816d5c12e';
abstract class _$ArticleContentProcessorService extends $Notifier<void> {
void build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/extensions/http_encoding.dart';
import 'package:weblibre/features/web_feed/data/models/feed_parse_result.dart';
import 'package:weblibre/features/web_feed/utils/feed_parser.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.bodyUnicodeFallback,
);
final result = FeedParseResult(
feedData: parser.readGeneralData(),
articleData: parser.readArticles(),
);
return result.toJson();
} finally {
client.close();
}
}, {'token': rootIsolateToken, 'url': url.toString()});
return FeedParseResult.fromJson(result);
}
@override
void build() {}
}
@@ -0,0 +1,61 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_reader.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(FeedReader)
final feedReaderProvider = FeedReaderProvider._();
final class FeedReaderProvider extends $NotifierProvider<FeedReader, void> {
FeedReaderProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'feedReaderProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$feedReaderHash();
@$internal
@override
FeedReader create() => FeedReader();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$feedReaderHash() => r'5d1ca364fe7ad702628a7f2bbd3e706876bc3111';
abstract class _$FeedReader extends $Notifier<void> {
void build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}