background fetch running

This commit is contained in:
Fabian Freund
2025-03-05 14:27:13 +01:00
parent 20bdc4df3e
commit 70d1c1b50e
62 changed files with 898 additions and 173 deletions
@@ -24,10 +24,45 @@ class ArticleDao extends DatabaseAccessor<FeedDatabase> with _$ArticleDaoMixin {
]);
}
Selectable<FeedArticle> getUnprocessedArticles() {
return db.articleView.select()..where(
(article) =>
(article.contentHtml.isNotNull() &
(article.contentMarkdown.isNull() |
article.contentPlain.isNull())) |
(article.summaryHtml.isNotNull() &
(article.summaryMarkdown.isNull() |
article.summaryPlain.isNull())),
);
}
SingleOrNullSelectable<FeedArticle> getArticleById(String articleId) {
return db.articleView.select()..where((row) => row.id.equals(articleId));
}
Future<void> updateArticleContent(List<FeedArticle> articles) {
return db.transaction(() async {
await Future.wait(
articles.map((newArticle) {
final statement =
db.article.update()
..where((article) => article.id.equals(newArticle.id));
return statement.write(
ArticleCompanion(
summaryHtml: Value(newArticle.summaryHtml),
summaryMarkdown: Value(newArticle.summaryMarkdown),
summaryPlain: Value(newArticle.summaryPlain),
contentHtml: Value(newArticle.contentHtml),
contentMarkdown: Value(newArticle.contentMarkdown),
contentPlain: Value(newArticle.contentPlain),
),
);
}),
);
});
}
Future<void> upsertArticles(List<FeedArticle> articles) {
return db.transaction(() async {
await Future.wait(
@@ -39,9 +74,11 @@ class ArticleDao extends DatabaseAccessor<FeedDatabase> with _$ArticleDaoMixin {
(old) {
return ArticleCompanion(
authors: Value(article.authors),
contentHtml: Value(article.contentHtml),
contentMarkdown: Value(article.contentMarkdown),
contentPlain: Value(article.contentPlain),
links: Value(article.links),
summaryHtml: Value(article.summaryHtml),
summaryMarkdown: Value(article.summaryMarkdown),
summaryPlain: Value(article.summaryPlain),
tags: Value(article.tags),
@@ -27,8 +27,10 @@ CREATE TABLE article (
authors TEXT MAPPED BY `const FeedAuthorsConverter()`,
tags TEXT MAPPED BY `const FeedCategoriesConverter()`,
links TEXT MAPPED BY `const FeedLinksConverter()`,
summaryHtml TEXT,
summaryMarkdown TEXT,
summaryPlain TEXT,
contentHtml TEXT,
contentMarkdown TEXT,
contentPlain TEXT
) WITH FeedArticle;
@@ -36,7 +38,8 @@ CREATE TABLE article (
CREATE VIEW article_view WITH FeedArticle AS
SELECT
a.*,
f.icon
f.icon,
f.site_link
FROM
article a
INNER JOIN
@@ -547,6 +547,14 @@ class Article extends Table with TableInfo<Article, FeedArticle> {
requiredDuringInsert: false,
$customConstraints: '',
).withConverter<List<FeedLink>?>(Article.$converterlinksn);
late final GeneratedColumn<String> summaryHtml = GeneratedColumn<String>(
'summaryHtml',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '',
);
late final GeneratedColumn<String> summaryMarkdown = GeneratedColumn<String>(
'summaryMarkdown',
aliasedName,
@@ -563,6 +571,14 @@ class Article extends Table with TableInfo<Article, FeedArticle> {
requiredDuringInsert: false,
$customConstraints: '',
);
late final GeneratedColumn<String> contentHtml = GeneratedColumn<String>(
'contentHtml',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '',
);
late final GeneratedColumn<String> contentMarkdown = GeneratedColumn<String>(
'contentMarkdown',
aliasedName,
@@ -591,8 +607,10 @@ class Article extends Table with TableInfo<Article, FeedArticle> {
authors,
tags,
links,
summaryHtml,
summaryMarkdown,
summaryPlain,
contentHtml,
contentMarkdown,
contentPlain,
];
@@ -657,6 +675,10 @@ class Article extends Table with TableInfo<Article, FeedArticle> {
data['${effectivePrefix}links'],
),
),
summaryHtml: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}summaryHtml'],
),
summaryMarkdown: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}summaryMarkdown'],
@@ -665,6 +687,10 @@ class Article extends Table with TableInfo<Article, FeedArticle> {
DriftSqlType.string,
data['${effectivePrefix}summaryPlain'],
),
contentHtml: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}contentHtml'],
),
contentMarkdown: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}contentMarkdown'],
@@ -709,8 +735,10 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
final Value<List<FeedAuthor>?> authors;
final Value<List<FeedCategory>?> tags;
final Value<List<FeedLink>?> links;
final Value<String?> summaryHtml;
final Value<String?> summaryMarkdown;
final Value<String?> summaryPlain;
final Value<String?> contentHtml;
final Value<String?> contentMarkdown;
final Value<String?> contentPlain;
final Value<int> rowid;
@@ -725,8 +753,10 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
this.authors = const Value.absent(),
this.tags = const Value.absent(),
this.links = const Value.absent(),
this.summaryHtml = const Value.absent(),
this.summaryMarkdown = const Value.absent(),
this.summaryPlain = const Value.absent(),
this.contentHtml = const Value.absent(),
this.contentMarkdown = const Value.absent(),
this.contentPlain = const Value.absent(),
this.rowid = const Value.absent(),
@@ -742,8 +772,10 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
this.authors = const Value.absent(),
this.tags = const Value.absent(),
this.links = const Value.absent(),
this.summaryHtml = const Value.absent(),
this.summaryMarkdown = const Value.absent(),
this.summaryPlain = const Value.absent(),
this.contentHtml = const Value.absent(),
this.contentMarkdown = const Value.absent(),
this.contentPlain = const Value.absent(),
this.rowid = const Value.absent(),
@@ -761,8 +793,10 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
Expression<String>? authors,
Expression<String>? tags,
Expression<String>? links,
Expression<String>? summaryHtml,
Expression<String>? summaryMarkdown,
Expression<String>? summaryPlain,
Expression<String>? contentHtml,
Expression<String>? contentMarkdown,
Expression<String>? contentPlain,
Expression<int>? rowid,
@@ -778,8 +812,10 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
if (authors != null) 'authors': authors,
if (tags != null) 'tags': tags,
if (links != null) 'links': links,
if (summaryHtml != null) 'summaryHtml': summaryHtml,
if (summaryMarkdown != null) 'summaryMarkdown': summaryMarkdown,
if (summaryPlain != null) 'summaryPlain': summaryPlain,
if (contentHtml != null) 'contentHtml': contentHtml,
if (contentMarkdown != null) 'contentMarkdown': contentMarkdown,
if (contentPlain != null) 'contentPlain': contentPlain,
if (rowid != null) 'rowid': rowid,
@@ -797,8 +833,10 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
Value<List<FeedAuthor>?>? authors,
Value<List<FeedCategory>?>? tags,
Value<List<FeedLink>?>? links,
Value<String?>? summaryHtml,
Value<String?>? summaryMarkdown,
Value<String?>? summaryPlain,
Value<String?>? contentHtml,
Value<String?>? contentMarkdown,
Value<String?>? contentPlain,
Value<int>? rowid,
@@ -814,8 +852,10 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
authors: authors ?? this.authors,
tags: tags ?? this.tags,
links: links ?? this.links,
summaryHtml: summaryHtml ?? this.summaryHtml,
summaryMarkdown: summaryMarkdown ?? this.summaryMarkdown,
summaryPlain: summaryPlain ?? this.summaryPlain,
contentHtml: contentHtml ?? this.contentHtml,
contentMarkdown: contentMarkdown ?? this.contentMarkdown,
contentPlain: contentPlain ?? this.contentPlain,
rowid: rowid ?? this.rowid,
@@ -861,12 +901,18 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
Article.$converterlinksn.toSql(links.value),
);
}
if (summaryHtml.present) {
map['summaryHtml'] = Variable<String>(summaryHtml.value);
}
if (summaryMarkdown.present) {
map['summaryMarkdown'] = Variable<String>(summaryMarkdown.value);
}
if (summaryPlain.present) {
map['summaryPlain'] = Variable<String>(summaryPlain.value);
}
if (contentHtml.present) {
map['contentHtml'] = Variable<String>(contentHtml.value);
}
if (contentMarkdown.present) {
map['contentMarkdown'] = Variable<String>(contentMarkdown.value);
}
@@ -892,8 +938,10 @@ class ArticleCompanion extends UpdateCompanion<FeedArticle> {
..write('authors: $authors, ')
..write('tags: $tags, ')
..write('links: $links, ')
..write('summaryHtml: $summaryHtml, ')
..write('summaryMarkdown: $summaryMarkdown, ')
..write('summaryPlain: $summaryPlain, ')
..write('contentHtml: $contentHtml, ')
..write('contentMarkdown: $contentMarkdown, ')
..write('contentPlain: $contentPlain, ')
..write('rowid: $rowid')
@@ -920,8 +968,10 @@ class ArticleView extends ViewInfo<ArticleView, FeedArticle>
authors,
tags,
links,
summaryHtml,
summaryMarkdown,
summaryPlain,
contentHtml,
contentMarkdown,
contentPlain,
icon,
@@ -991,6 +1041,10 @@ class ArticleView extends ViewInfo<ArticleView, FeedArticle>
data['${effectivePrefix}links'],
),
),
summaryHtml: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}summaryHtml'],
),
summaryMarkdown: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}summaryMarkdown'],
@@ -999,6 +1053,10 @@ class ArticleView extends ViewInfo<ArticleView, FeedArticle>
DriftSqlType.string,
data['${effectivePrefix}summaryPlain'],
),
contentHtml: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}contentHtml'],
),
contentMarkdown: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}contentMarkdown'],
@@ -1080,6 +1138,12 @@ class ArticleView extends ViewInfo<ArticleView, FeedArticle>
true,
type: DriftSqlType.string,
).withConverter<List<FeedLink>?>(Article.$converterlinksn);
late final GeneratedColumn<String> summaryHtml = GeneratedColumn<String>(
'summaryHtml',
aliasedName,
true,
type: DriftSqlType.string,
);
late final GeneratedColumn<String> summaryMarkdown = GeneratedColumn<String>(
'summaryMarkdown',
aliasedName,
@@ -1092,6 +1156,12 @@ class ArticleView extends ViewInfo<ArticleView, FeedArticle>
true,
type: DriftSqlType.string,
);
late final GeneratedColumn<String> contentHtml = GeneratedColumn<String>(
'contentHtml',
aliasedName,
true,
type: DriftSqlType.string,
);
late final GeneratedColumn<String> contentMarkdown = GeneratedColumn<String>(
'contentMarkdown',
aliasedName,
@@ -1420,8 +1490,10 @@ abstract class _$FeedDatabase extends GeneratedDatabase {
Article.$converterlinks,
row.readNullable<String>('links'),
),
summaryHtml: row.readNullable<String>('summaryHtml'),
summaryMarkdown: row.readNullable<String>('summaryMarkdown'),
summaryPlain: row.readNullable<String>('summaryPlain'),
contentHtml: row.readNullable<String>('contentHtml'),
contentMarkdown: row.readNullable<String>('contentMarkdown'),
contentPlain: row.readNullable<String>('contentPlain'),
icon: NullAwareTypeConverter.wrapFromSql(
@@ -1473,8 +1545,10 @@ abstract class _$FeedDatabase extends GeneratedDatabase {
Article.$converterlinks,
row.readNullable<String>('links'),
),
summaryHtml: row.readNullable<String>('summaryHtml'),
summaryMarkdown: row.readNullable<String>('summaryMarkdown'),
summaryPlain: row.readNullable<String>('summaryPlain'),
contentHtml: row.readNullable<String>('contentHtml'),
contentMarkdown: row.readNullable<String>('contentMarkdown'),
contentPlain: row.readNullable<String>('contentPlain'),
icon: NullAwareTypeConverter.wrapFromSql(
@@ -1911,8 +1985,10 @@ typedef $ArticleCreateCompanionBuilder =
Value<List<FeedAuthor>?> authors,
Value<List<FeedCategory>?> tags,
Value<List<FeedLink>?> links,
Value<String?> summaryHtml,
Value<String?> summaryMarkdown,
Value<String?> summaryPlain,
Value<String?> contentHtml,
Value<String?> contentMarkdown,
Value<String?> contentPlain,
Value<int> rowid,
@@ -1929,8 +2005,10 @@ typedef $ArticleUpdateCompanionBuilder =
Value<List<FeedAuthor>?> authors,
Value<List<FeedCategory>?> tags,
Value<List<FeedLink>?> links,
Value<String?> summaryHtml,
Value<String?> summaryMarkdown,
Value<String?> summaryPlain,
Value<String?> contentHtml,
Value<String?> contentMarkdown,
Value<String?> contentPlain,
Value<int> rowid,
@@ -2018,6 +2096,11 @@ class $ArticleFilterComposer extends Composer<_$FeedDatabase, Article> {
builder: (column) => ColumnWithTypeConverterFilters(column),
);
ColumnFilters<String> get summaryHtml => $composableBuilder(
column: $table.summaryHtml,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get summaryMarkdown => $composableBuilder(
column: $table.summaryMarkdown,
builder: (column) => ColumnFilters(column),
@@ -2028,6 +2111,11 @@ class $ArticleFilterComposer extends Composer<_$FeedDatabase, Article> {
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get contentHtml => $composableBuilder(
column: $table.contentHtml,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get contentMarkdown => $composableBuilder(
column: $table.contentMarkdown,
builder: (column) => ColumnFilters(column),
@@ -2115,6 +2203,11 @@ class $ArticleOrderingComposer extends Composer<_$FeedDatabase, Article> {
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get summaryHtml => $composableBuilder(
column: $table.summaryHtml,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get summaryMarkdown => $composableBuilder(
column: $table.summaryMarkdown,
builder: (column) => ColumnOrderings(column),
@@ -2125,6 +2218,11 @@ class $ArticleOrderingComposer extends Composer<_$FeedDatabase, Article> {
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get contentHtml => $composableBuilder(
column: $table.contentHtml,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get contentMarkdown => $composableBuilder(
column: $table.contentMarkdown,
builder: (column) => ColumnOrderings(column),
@@ -2194,6 +2292,11 @@ class $ArticleAnnotationComposer extends Composer<_$FeedDatabase, Article> {
GeneratedColumnWithTypeConverter<List<FeedLink>?, String> get links =>
$composableBuilder(column: $table.links, builder: (column) => column);
GeneratedColumn<String> get summaryHtml => $composableBuilder(
column: $table.summaryHtml,
builder: (column) => column,
);
GeneratedColumn<String> get summaryMarkdown => $composableBuilder(
column: $table.summaryMarkdown,
builder: (column) => column,
@@ -2204,6 +2307,11 @@ class $ArticleAnnotationComposer extends Composer<_$FeedDatabase, Article> {
builder: (column) => column,
);
GeneratedColumn<String> get contentHtml => $composableBuilder(
column: $table.contentHtml,
builder: (column) => column,
);
GeneratedColumn<String> get contentMarkdown => $composableBuilder(
column: $table.contentMarkdown,
builder: (column) => column,
@@ -2276,8 +2384,10 @@ class $ArticleTableManager
Value<List<FeedAuthor>?> authors = const Value.absent(),
Value<List<FeedCategory>?> tags = const Value.absent(),
Value<List<FeedLink>?> links = const Value.absent(),
Value<String?> summaryHtml = const Value.absent(),
Value<String?> summaryMarkdown = const Value.absent(),
Value<String?> summaryPlain = const Value.absent(),
Value<String?> contentHtml = const Value.absent(),
Value<String?> contentMarkdown = const Value.absent(),
Value<String?> contentPlain = const Value.absent(),
Value<int> rowid = const Value.absent(),
@@ -2292,8 +2402,10 @@ class $ArticleTableManager
authors: authors,
tags: tags,
links: links,
summaryHtml: summaryHtml,
summaryMarkdown: summaryMarkdown,
summaryPlain: summaryPlain,
contentHtml: contentHtml,
contentMarkdown: contentMarkdown,
contentPlain: contentPlain,
rowid: rowid,
@@ -2310,8 +2422,10 @@ class $ArticleTableManager
Value<List<FeedAuthor>?> authors = const Value.absent(),
Value<List<FeedCategory>?> tags = const Value.absent(),
Value<List<FeedLink>?> links = const Value.absent(),
Value<String?> summaryHtml = const Value.absent(),
Value<String?> summaryMarkdown = const Value.absent(),
Value<String?> summaryPlain = const Value.absent(),
Value<String?> contentHtml = const Value.absent(),
Value<String?> contentMarkdown = const Value.absent(),
Value<String?> contentPlain = const Value.absent(),
Value<int> rowid = const Value.absent(),
@@ -2326,8 +2440,10 @@ class $ArticleTableManager
authors: authors,
tags: tags,
links: links,
summaryHtml: summaryHtml,
summaryMarkdown: summaryMarkdown,
summaryPlain: summaryPlain,
contentHtml: contentHtml,
contentMarkdown: contentMarkdown,
contentPlain: contentPlain,
rowid: rowid,
@@ -1,3 +1,4 @@
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:drift/drift.dart';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:json_annotation/json_annotation.dart';
@@ -9,6 +10,7 @@ import 'package:lensai/features/web_feed/data/models/feed_link.dart';
part 'feed_article.g.dart';
@JsonSerializable()
@CopyWith()
class FeedArticle with FastEquatable implements Insertable<FeedArticle> {
final String id;
final Uri feedId;
@@ -20,13 +22,16 @@ class FeedArticle with FastEquatable implements Insertable<FeedArticle> {
final List<FeedAuthor>? authors;
final List<FeedCategory>? tags;
final List<FeedLink>? links;
final String? summaryHtml;
final String? summaryMarkdown;
final String? summaryPlain;
final String? contentHtml;
final String? contentMarkdown;
final String? contentPlain;
//Derived by view from feed table, should not get inserted
final Uri? icon;
final Uri? siteLink;
FeedArticle({
required this.id,
@@ -39,11 +44,14 @@ class FeedArticle with FastEquatable implements Insertable<FeedArticle> {
this.authors,
this.tags,
this.links,
this.summaryHtml,
this.summaryMarkdown,
this.summaryPlain,
this.contentHtml,
this.contentMarkdown,
this.contentPlain,
this.icon,
this.siteLink,
});
factory FeedArticle.fromJson(Map<String, dynamic> json) =>
@@ -82,12 +90,18 @@ class FeedArticle with FastEquatable implements Insertable<FeedArticle> {
if (!nullToAbsent || links != null) {
map['links'] = Variable<String>(Article.$converterlinksn.toSql(links));
}
if (!nullToAbsent || summaryHtml != null) {
map['summaryHtml'] = Variable<String>(summaryHtml);
}
if (!nullToAbsent || summaryMarkdown != null) {
map['summaryMarkdown'] = Variable<String>(summaryMarkdown);
}
if (!nullToAbsent || summaryPlain != null) {
map['summaryPlain'] = Variable<String>(summaryPlain);
}
if (!nullToAbsent || contentHtml != null) {
map['contentHtml'] = Variable<String>(contentHtml);
}
if (!nullToAbsent || contentMarkdown != null) {
map['contentMarkdown'] = Variable<String>(contentMarkdown);
}
@@ -109,10 +123,13 @@ class FeedArticle with FastEquatable implements Insertable<FeedArticle> {
authors,
tags,
links,
summaryHtml,
summaryMarkdown,
summaryPlain,
contentHtml,
contentMarkdown,
contentPlain,
icon,
siteLink,
];
}
@@ -2,6 +2,257 @@
part of 'feed_article.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$FeedArticleCWProxy {
FeedArticle id(String id);
FeedArticle feedId(Uri feedId);
FeedArticle fetched(DateTime fetched);
FeedArticle created(DateTime? created);
FeedArticle updated(DateTime? updated);
FeedArticle lastRead(DateTime? lastRead);
FeedArticle title(String? title);
FeedArticle authors(List<FeedAuthor>? authors);
FeedArticle tags(List<FeedCategory>? tags);
FeedArticle links(List<FeedLink>? links);
FeedArticle summaryHtml(String? summaryHtml);
FeedArticle summaryMarkdown(String? summaryMarkdown);
FeedArticle summaryPlain(String? summaryPlain);
FeedArticle contentHtml(String? contentHtml);
FeedArticle contentMarkdown(String? contentMarkdown);
FeedArticle contentPlain(String? contentPlain);
FeedArticle icon(Uri? icon);
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FeedArticle(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// FeedArticle(...).copyWith(id: 12, name: "My name")
/// ````
FeedArticle call({
String id,
Uri feedId,
DateTime fetched,
DateTime? created,
DateTime? updated,
DateTime? lastRead,
String? title,
List<FeedAuthor>? authors,
List<FeedCategory>? tags,
List<FeedLink>? links,
String? summaryHtml,
String? summaryMarkdown,
String? summaryPlain,
String? contentHtml,
String? contentMarkdown,
String? contentPlain,
Uri? icon,
});
}
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfFeedArticle.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfFeedArticle.copyWith.fieldName(...)`
class _$FeedArticleCWProxyImpl implements _$FeedArticleCWProxy {
const _$FeedArticleCWProxyImpl(this._value);
final FeedArticle _value;
@override
FeedArticle id(String id) => this(id: id);
@override
FeedArticle feedId(Uri feedId) => this(feedId: feedId);
@override
FeedArticle fetched(DateTime fetched) => this(fetched: fetched);
@override
FeedArticle created(DateTime? created) => this(created: created);
@override
FeedArticle updated(DateTime? updated) => this(updated: updated);
@override
FeedArticle lastRead(DateTime? lastRead) => this(lastRead: lastRead);
@override
FeedArticle title(String? title) => this(title: title);
@override
FeedArticle authors(List<FeedAuthor>? authors) => this(authors: authors);
@override
FeedArticle tags(List<FeedCategory>? tags) => this(tags: tags);
@override
FeedArticle links(List<FeedLink>? links) => this(links: links);
@override
FeedArticle summaryHtml(String? summaryHtml) =>
this(summaryHtml: summaryHtml);
@override
FeedArticle summaryMarkdown(String? summaryMarkdown) =>
this(summaryMarkdown: summaryMarkdown);
@override
FeedArticle summaryPlain(String? summaryPlain) =>
this(summaryPlain: summaryPlain);
@override
FeedArticle contentHtml(String? contentHtml) =>
this(contentHtml: contentHtml);
@override
FeedArticle contentMarkdown(String? contentMarkdown) =>
this(contentMarkdown: contentMarkdown);
@override
FeedArticle contentPlain(String? contentPlain) =>
this(contentPlain: contentPlain);
@override
FeedArticle icon(Uri? icon) => this(icon: icon);
@override
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FeedArticle(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// FeedArticle(...).copyWith(id: 12, name: "My name")
/// ````
FeedArticle call({
Object? id = const $CopyWithPlaceholder(),
Object? feedId = const $CopyWithPlaceholder(),
Object? fetched = const $CopyWithPlaceholder(),
Object? created = const $CopyWithPlaceholder(),
Object? updated = const $CopyWithPlaceholder(),
Object? lastRead = const $CopyWithPlaceholder(),
Object? title = const $CopyWithPlaceholder(),
Object? authors = const $CopyWithPlaceholder(),
Object? tags = const $CopyWithPlaceholder(),
Object? links = const $CopyWithPlaceholder(),
Object? summaryHtml = const $CopyWithPlaceholder(),
Object? summaryMarkdown = const $CopyWithPlaceholder(),
Object? summaryPlain = const $CopyWithPlaceholder(),
Object? contentHtml = const $CopyWithPlaceholder(),
Object? contentMarkdown = const $CopyWithPlaceholder(),
Object? contentPlain = const $CopyWithPlaceholder(),
Object? icon = const $CopyWithPlaceholder(),
}) {
return FeedArticle(
id:
id == const $CopyWithPlaceholder()
? _value.id
// ignore: cast_nullable_to_non_nullable
: id as String,
feedId:
feedId == const $CopyWithPlaceholder()
? _value.feedId
// ignore: cast_nullable_to_non_nullable
: feedId as Uri,
fetched:
fetched == const $CopyWithPlaceholder()
? _value.fetched
// ignore: cast_nullable_to_non_nullable
: fetched as DateTime,
created:
created == const $CopyWithPlaceholder()
? _value.created
// ignore: cast_nullable_to_non_nullable
: created as DateTime?,
updated:
updated == const $CopyWithPlaceholder()
? _value.updated
// ignore: cast_nullable_to_non_nullable
: updated as DateTime?,
lastRead:
lastRead == const $CopyWithPlaceholder()
? _value.lastRead
// ignore: cast_nullable_to_non_nullable
: lastRead as DateTime?,
title:
title == const $CopyWithPlaceholder()
? _value.title
// ignore: cast_nullable_to_non_nullable
: title as String?,
authors:
authors == const $CopyWithPlaceholder()
? _value.authors
// ignore: cast_nullable_to_non_nullable
: authors as List<FeedAuthor>?,
tags:
tags == const $CopyWithPlaceholder()
? _value.tags
// ignore: cast_nullable_to_non_nullable
: tags as List<FeedCategory>?,
links:
links == const $CopyWithPlaceholder()
? _value.links
// ignore: cast_nullable_to_non_nullable
: links as List<FeedLink>?,
summaryHtml:
summaryHtml == const $CopyWithPlaceholder()
? _value.summaryHtml
// ignore: cast_nullable_to_non_nullable
: summaryHtml as String?,
summaryMarkdown:
summaryMarkdown == const $CopyWithPlaceholder()
? _value.summaryMarkdown
// ignore: cast_nullable_to_non_nullable
: summaryMarkdown as String?,
summaryPlain:
summaryPlain == const $CopyWithPlaceholder()
? _value.summaryPlain
// ignore: cast_nullable_to_non_nullable
: summaryPlain as String?,
contentHtml:
contentHtml == const $CopyWithPlaceholder()
? _value.contentHtml
// ignore: cast_nullable_to_non_nullable
: contentHtml as String?,
contentMarkdown:
contentMarkdown == const $CopyWithPlaceholder()
? _value.contentMarkdown
// ignore: cast_nullable_to_non_nullable
: contentMarkdown as String?,
contentPlain:
contentPlain == const $CopyWithPlaceholder()
? _value.contentPlain
// ignore: cast_nullable_to_non_nullable
: contentPlain as String?,
icon:
icon == const $CopyWithPlaceholder()
? _value.icon
// ignore: cast_nullable_to_non_nullable
: icon as Uri?,
);
}
}
extension $FeedArticleCopyWith on FeedArticle {
/// Returns a callable class that can be used as follows: `instanceOfFeedArticle.copyWith(...)` or like so:`instanceOfFeedArticle.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$FeedArticleCWProxy get copyWith => _$FeedArticleCWProxyImpl(this);
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
@@ -35,8 +286,10 @@ FeedArticle _$FeedArticleFromJson(Map<String, dynamic> json) => FeedArticle(
(json['links'] as List<dynamic>?)
?.map((e) => FeedLink.fromJson(e as Map<String, dynamic>))
.toList(),
summaryHtml: json['summaryHtml'] as String?,
summaryMarkdown: json['summaryMarkdown'] as String?,
summaryPlain: json['summaryPlain'] as String?,
contentHtml: json['contentHtml'] as String?,
contentMarkdown: json['contentMarkdown'] as String?,
contentPlain: json['contentPlain'] as String?,
icon: json['icon'] == null ? null : Uri.parse(json['icon'] as String),
@@ -54,8 +307,10 @@ Map<String, dynamic> _$FeedArticleToJson(FeedArticle instance) =>
'authors': instance.authors?.map((e) => e.toJson()).toList(),
'tags': instance.tags?.map((e) => e.toJson()).toList(),
'links': instance.links?.map((e) => e.toJson()).toList(),
'summaryHtml': instance.summaryHtml,
'summaryMarkdown': instance.summaryMarkdown,
'summaryPlain': instance.summaryPlain,
'contentHtml': instance.contentHtml,
'contentMarkdown': instance.contentMarkdown,
'contentPlain': instance.contentPlain,
'icon': instance.icon?.toString(),
@@ -19,8 +19,10 @@ class FeedArticleQueryResult extends FeedArticle {
required super.authors,
required super.tags,
required super.links,
required super.summaryHtml,
required super.summaryMarkdown,
required super.summaryPlain,
required super.contentHtml,
required super.contentMarkdown,
required super.contentPlain,
required super.icon,
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io';
import 'package:drift/drift.dart';
@@ -38,8 +37,8 @@ FeedDatabase feedDatabase(Ref ref) {
}),
);
ref.onDispose(() {
unawaited(db.close());
ref.onDispose(() async {
await db.close();
});
return db;
@@ -6,7 +6,7 @@ part of 'providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$feedDatabaseHash() => r'a4a1ffa36b73c2aecbe17bf096e30fa9b9ad23c9';
String _$feedDatabaseHash() => r'c3b20e867da5af6e92d1e8c1efa96b79988837a1';
/// See also [feedDatabase].
@ProviderFor(feedDatabase)
@@ -10,6 +10,7 @@ import 'package:lensai/features/web_feed/domain/repositories/feed_repository.dar
import 'package:lensai/features/web_feed/domain/services/feed_reader.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
part 'providers.g.dart';
@@ -53,7 +54,7 @@ class ArticleSearch extends _$ArticleSearch {
await _streamController.close();
});
return _streamController.stream;
return ConcatStream([Stream.value([]), _streamController.stream]);
}
}
@@ -827,7 +827,7 @@ class _ArticleSearchProviderElement
}
String _$filteredArticleListHash() =>
r'4443a4a92cab0c8f534a74a1cba681a8973b058e';
r'a691c3c6aa722dd85ee4980e6c48721a8025f1d8';
abstract class _$FilteredArticleList
extends BuildlessAutoDisposeNotifier<AsyncValue<List<FeedArticle>>> {
@@ -0,0 +1,51 @@
import 'package:collection/collection.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/core/logger.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/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.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.whenNotEmpty,
contentPlain: content[index].plain.whenNotEmpty,
summaryMarkdown: summary[index].markdown.whenNotEmpty,
summaryPlain: summary[index].plain.whenNotEmpty,
),
)
.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,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'article_content_processor.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$articleContentProcessorServiceHash() =>
r'182cd89e38a0a9a86b7425c16ad2bd1fc39c6704';
/// See also [ArticleContentProcessorService].
@ProviderFor(ArticleContentProcessorService)
final articleContentProcessorServiceProvider =
NotifierProvider<ArticleContentProcessorService, void>.internal(
ArticleContentProcessorService.new,
name: r'articleContentProcessorServiceProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$articleContentProcessorServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ArticleContentProcessorService = 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
@@ -28,7 +28,7 @@ class FeedReader extends _$FeedReader {
final parser = FeedParser.parse(url: url, xmlString: response.body);
final result = FeedParseResult(
feedData: parser.readGeneralData(),
articleData: await parser.readArticles(),
articleData: parser.readArticles(),
);
return result.toJson();
@@ -6,7 +6,7 @@ part of 'feed_reader.dart';
// RiverpodGenerator
// **************************************************************************
String _$feedReaderHash() => r'67b09485e40c522108709ca32b9c624b4db2446b';
String _$feedReaderHash() => r'301e01fa831c58723dc0ab118496f4fb34c38610';
/// See also [FeedReader].
@ProviderFor(FeedReader)
@@ -1,3 +1,4 @@
import 'package:lensai/core/logger.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_annotation/riverpod_annotation.dart';
@@ -14,17 +15,25 @@ class FetchArticlesController extends _$FetchArticlesController {
await Future.wait(
feeds.map((feed) async {
final result = await ref
.read(feedReaderProvider.notifier)
.parseFeed(feed.url);
try {
final result = await ref
.read(feedReaderProvider.notifier)
.parseFeed(feed.url);
await ref
.read(feedRepositoryProvider.notifier)
.upsertArticles(result.articleData);
await ref
.read(feedRepositoryProvider.notifier)
.upsertArticles(result.articleData);
await ref
.read(feedRepositoryProvider.notifier)
.touchFeedFetched(feed.url);
await ref
.read(feedRepositoryProvider.notifier)
.touchFeedFetched(feed.url);
} catch (e, s) {
logger.e(
'Failed fetching feed ${feed.url}',
error: e,
stackTrace: s,
);
}
}).toList(),
);
});
@@ -7,7 +7,7 @@ part of 'fetch_articles.dart';
// **************************************************************************
String _$fetchArticlesControllerHash() =>
r'63da7f6ba9eab28aebff4c60d98e2f2a404c5138';
r'7604d9352da886624c3cf6862b89c397edda1f28';
/// See also [FetchArticlesController].
@ProviderFor(FetchArticlesController)
@@ -6,8 +6,10 @@ 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/data/models/feed_link.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/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';
@@ -52,7 +54,12 @@ class FeedArticleCard extends HookConsumerWidget {
Row(
children: [
UrlIcon([
article.icon ?? article.feedId.base,
article.icon ??
article.links
?.getRelation(FeedLinkRelation.alternate)
?.uri ??
article.siteLink ??
article.feedId.base,
], iconSize: 34.0),
const SizedBox(width: 12.0),
Expanded(
@@ -1,5 +1,4 @@
import 'package:collection/collection.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
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_article.dart';
@@ -79,28 +78,11 @@ class FeedParser {
}
}
Future<List<FeedArticle>> readArticles() async {
List<FeedArticle> readArticles() {
final fetchDate = DateTime.now();
switch (_feed) {
case final Rss1Feed feed:
final processedContents =
await GeckoBrowserExtensionService.turndownHtml(
feed.items.map((item) => item.content?.value ?? '').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;
@@ -119,32 +101,19 @@ class FeedParser {
authors: item.dc?.creator.whenNotEmpty.mapNotNull(
(creator) => [FeedAuthor(name: creator)],
),
summaryPlain: processedSummaries[i].plain.whenNotEmpty,
summaryMarkdown: processedSummaries[i].markdown.whenNotEmpty,
links: link.mapNotNull((link) => [FeedLink(uri: link)]),
summaryHtml:
item.description.whenNotEmpty ??
item.dc?.description.whenNotEmpty,
links: link.mapNotNull(
(link) => [
FeedLink(uri: link, relation: FeedLinkRelation.alternate),
],
),
tags: item.dc?.toFeedCategories(),
contentPlain: processedContents[i].plain.trim().whenNotEmpty,
contentMarkdown: processedContents[i].markdown?.trim().whenNotEmpty,
contentHtml: item.content?.value.whenNotEmpty,
);
}).toList();
case final RssFeed feed:
final processedContents =
await GeckoBrowserExtensionService.turndownHtml(
feed.items.map((item) => item.content?.value ?? '').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;
@@ -173,28 +142,22 @@ class FeedParser {
authors: author.mapNotNull(
(creator) => [FeedAuthor(name: creator)],
),
summaryPlain: processedSummaries[i].plain.whenNotEmpty,
summaryMarkdown: processedSummaries[i].markdown.whenNotEmpty,
links: link.mapNotNull((link) => [FeedLink(uri: link)]),
summaryHtml:
item.description.whenNotEmpty ??
item.dc?.description.whenNotEmpty,
links: link.mapNotNull(
(link) => [
FeedLink(uri: link, relation: FeedLinkRelation.alternate),
],
),
tags:
categories.isNotEmpty
? categories
: item.dc?.toFeedCategories(),
contentPlain: processedContents[i].plain.trim().whenNotEmpty,
contentMarkdown: processedContents[i].markdown?.trim().whenNotEmpty,
contentHtml: item.content?.value.whenNotEmpty,
);
}).toList();
case final AtomFeed feed:
final processedContents =
await GeckoBrowserExtensionService.turndownHtml(
feed.items.map((item) => item.content ?? '').toList(),
);
final processedSummaries =
await GeckoBrowserExtensionService.turndownHtml(
feed.items.map((item) => item.summary ?? '').toList(),
);
final feedLink = feed.links.toFeedLinks().getRelation(
FeedLinkRelation.self,
);
@@ -218,16 +181,14 @@ class FeedParser {
id: uniqueId,
feedId: url,
title: item.title.whenNotEmpty,
summaryPlain: processedSummaries[i].plain.whenNotEmpty,
summaryMarkdown: processedSummaries[i].markdown.whenNotEmpty,
summaryHtml: item.summary.whenNotEmpty,
links: itemLinks,
authors: authors.isNotEmpty ? authors : null,
tags: tags,
fetched: fetchDate,
created: published,
updated: updated,
contentPlain: processedContents[i].plain.trim().whenNotEmpty,
contentMarkdown: processedContents[i].markdown?.trim().whenNotEmpty,
contentHtml: item.content.whenNotEmpty,
);
}).toList();
default:
@@ -0,0 +1,35 @@
import 'package:background_fetch/background_fetch.dart';
import 'package:lensai/core/error_observer.dart';
import 'package:lensai/core/logger.dart';
import 'package:lensai/features/web_feed/presentation/controllers/fetch_articles.dart';
import 'package:riverpod/riverpod.dart';
@pragma('vm:entry-point')
Future<void> backgroundFetch(HeadlessTask task) async {
final taskId = task.taskId;
final isTimeout = task.timeout;
if (isTimeout) {
// This task has exceeded its allowed running-time.
// You must stop what you're doing and immediately .finish(taskId)
logger.e("[BackgroundFetch] Headless task timed-out: $taskId");
await BackgroundFetch.finish(taskId);
return;
}
final ref = ProviderContainer(observers: const [ErrorObserver()]);
try {
await ref.read(fetchArticlesControllerProvider.notifier).fetchAllArticles();
logger.i('Fetched articles in background');
} catch (e, s) {
logger.e('Failed fetching articles', error: e, stackTrace: s);
} finally {
ref.dispose();
// Give everything a bit of time to properly dispose
await Future.delayed(const Duration(seconds: 1));
await BackgroundFetch.finish(taskId);
}
}