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,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 'dart:convert';
import 'package:drift/drift.dart';
import 'package:weblibre/features/web_feed/data/models/feed_author.dart';
class FeedAuthorsConverter extends TypeConverter<List<FeedAuthor>, String> {
const FeedAuthorsConverter();
@override
List<FeedAuthor> fromSql(String fromDb) {
final authors = jsonDecode(fromDb) as List<dynamic>;
return authors
.map((author) => FeedAuthor.fromJson(author as Map<String, dynamic>))
.toList();
}
@override
String toSql(List<FeedAuthor> value) {
return jsonEncode(value.map((author) => author.toJson()).toList());
}
}
@@ -0,0 +1,43 @@
/*
* 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:convert';
import 'package:drift/drift.dart';
import 'package:weblibre/features/web_feed/data/models/feed_category.dart';
class FeedCategoriesConverter
extends TypeConverter<List<FeedCategory>, String> {
const FeedCategoriesConverter();
@override
List<FeedCategory> fromSql(String fromDb) {
final categories = jsonDecode(fromDb) as List<dynamic>;
return categories
.map(
(category) => FeedCategory.fromJson(category as Map<String, dynamic>),
)
.toList();
}
@override
String toSql(List<FeedCategory> value) {
return jsonEncode(value.map((category) => category.toJson()).toList());
}
}
@@ -0,0 +1,35 @@
/*
* 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:json_annotation/json_annotation.dart';
import 'package:weblibre/features/web_feed/data/database/definitions.drift.dart';
class FeedDataConverter extends JsonConverter<FeedData, Map<String, dynamic>> {
const FeedDataConverter();
@override
FeedData fromJson(Map<String, dynamic> json) {
return FeedData.fromJson(json);
}
@override
Map<String, dynamic> toJson(FeedData object) {
return object.toJson();
}
}
@@ -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 'dart:convert';
import 'package:drift/drift.dart';
import 'package:weblibre/features/web_feed/data/models/feed_link.dart';
class FeedLinksConverter extends TypeConverter<List<FeedLink>, String> {
const FeedLinksConverter();
@override
List<FeedLink> fromSql(String fromDb) {
final links = jsonDecode(fromDb) as List<dynamic>;
return links
.map((link) => FeedLink.fromJson(link as Map<String, dynamic>))
.toList();
}
@override
String toSql(List<FeedLink> value) {
return jsonEncode(value.map((link) => link.toJson()).toList());
}
}
@@ -0,0 +1,173 @@
/*
* 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:drift/drift.dart';
import 'package:weblibre/features/web_feed/data/database/daos/article.drift.dart';
import 'package:weblibre/features/web_feed/data/database/database.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_article_query_result.dart';
@DriftAccessor()
class ArticleDao extends DatabaseAccessor<FeedDatabase> with $ArticleDaoMixin {
ArticleDao(super.attachedDatabase);
Selectable<FeedArticle> getFeedArticles(Uri? url) {
final select = db.articleView.select();
if (url != null) {
select.where((article) => article.feedId.equalsValue(url));
}
return select..orderBy([
(row) => OrderingTerm(
expression: coalesce([row.updated, row.created]),
mode: OrderingMode.desc,
),
]);
}
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(
articles
.map(
(article) => db.article.insertOne(
article,
onConflict: DoUpdate(
(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),
title: Value(article.title),
updated: Value(article.updated),
);
},
where: (old) =>
old.updated.isNotNull() &
old.updated.isSmallerThanValue(
article.updated ?? DateTime(0),
),
),
),
)
.toList(),
);
});
}
Future<int> updateArticleRead(String articleId, DateTime? read) {
final statement = db.article.update()
..where((article) => article.id.equals(articleId));
return statement.write(ArticleCompanion(lastRead: Value(read)));
}
Selectable<(String, int)> getUnreadArticleCount() {
final count = countAll();
final countByFeed = db.article.selectOnly()
..addColumns([db.article.feedId, count])
..where(
db.article.lastRead.isNull() |
(db.article.updated.isNotNull() &
db.article.lastRead.isSmallerThan(db.article.lastRead)),
)
..groupBy([db.article.feedId]);
return countByFeed.map(
(result) => (result.read(db.article.feedId)!, result.read(count)!),
);
}
Selectable<FeedArticleQueryResult> queryArticles({
required String matchPrefix,
required String matchSuffix,
required String ellipsis,
required int snippetLength,
required String searchString,
required Uri? feedId,
int limit = 25,
}) {
final ftsQuery = db.buildFtsQuery(searchString);
if (ftsQuery.isNotEmpty) {
return db.definitionsDrift.queryArticlesFullContent(
feedId: feedId?.toString(),
query: ftsQuery,
snippetLength: snippetLength,
beforeMatch: matchPrefix,
afterMatch: matchSuffix,
ellipsis: ellipsis,
limit: limit,
);
} else {
return db.definitionsDrift.queryArticlesBasic(
feedId: feedId?.toString(),
query: db.buildLikeQuery(searchString),
limit: limit,
);
}
}
}
@@ -0,0 +1,13 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/web_feed/data/database/database.dart' as i1;
mixin $ArticleDaoMixin on i0.DatabaseAccessor<i1.FeedDatabase> {
ArticleDaoManager get managers => ArticleDaoManager(this);
}
class ArticleDaoManager {
final $ArticleDaoMixin _db;
ArticleDaoManager(this._db);
}
@@ -0,0 +1,61 @@
/*
* 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:drift/drift.dart';
import 'package:weblibre/features/web_feed/data/database/daos/feed.drift.dart';
import 'package:weblibre/features/web_feed/data/database/database.dart';
import 'package:weblibre/features/web_feed/data/database/definitions.drift.dart';
@DriftAccessor()
class FeedDao extends DatabaseAccessor<FeedDatabase> with $FeedDaoMixin {
FeedDao(super.attachedDatabase);
Selectable<FeedData> getFeeds() {
return db.feed.select();
}
SingleOrNullSelectable<FeedData> getFeed(Uri feedId) {
return db.feed.select()..where((feed) => feed.url.equalsValue(feedId));
}
Future<int> updateFeedFetched(Uri feedId, DateTime fetched) {
final statement = db.feed.update()
..where((feed) => feed.url.equalsValue(feedId));
return statement.write(FeedCompanion(lastFetched: Value(fetched)));
}
Future<int> deleteFeed(Uri feedId) {
return db.feed.deleteWhere((feed) => feed.url.equals(feedId.toString()));
}
Future<int> upsertFeed(FeedData feedData) {
return db.feed.insertOne(
feedData,
onConflict: DoUpdate((old) {
return FeedCompanion(
authors: Value(feedData.authors),
title: Value(feedData.title),
description: Value(feedData.description),
tags: Value(feedData.tags),
);
}),
);
}
}
@@ -0,0 +1,13 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/web_feed/data/database/database.dart' as i1;
mixin $FeedDaoMixin on i0.DatabaseAccessor<i1.FeedDatabase> {
FeedDaoManager get managers => FeedDaoManager(this);
}
class FeedDaoManager {
final $FeedDaoMixin _db;
FeedDaoManager(this._db);
}
@@ -0,0 +1,53 @@
/*
* 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:drift/drift.dart';
import 'package:drift_dev/api/migrations_native.dart';
import 'package:flutter/foundation.dart';
import 'package:weblibre/features/search/domain/fts_tokenizer.dart';
import 'package:weblibre/features/web_feed/data/database/daos/article.dart';
import 'package:weblibre/features/web_feed/data/database/daos/feed.dart';
import 'package:weblibre/features/web_feed/data/database/database.drift.dart';
@DriftDatabase(include: {'definitions.drift'}, daos: [ArticleDao, FeedDao])
class FeedDatabase extends $FeedDatabase with TrigramQueryBuilderMixin {
@override
final int schemaVersion = 1;
@override
final int ftsTokenLimit = 10;
@override
final int ftsMinTokenLength = 3;
@override
MigrationStrategy get migration => MigrationStrategy(
beforeOpen: (details) async {
if (kDebugMode) {
// This check pulls in a fair amount of code that's not needed
// anywhere else, so we recommend only doing it in debug builds.
await validateDatabaseSchema();
}
await customStatement('PRAGMA foreign_keys = ON;');
await definitionsDrift.optimizeFtsIndex();
},
);
FeedDatabase(super.e);
}
@@ -0,0 +1,127 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/web_feed/data/database/definitions.drift.dart'
as i1;
import 'package:weblibre/features/web_feed/data/database/daos/article.dart'
as i2;
import 'package:weblibre/features/web_feed/data/database/database.dart' as i3;
import 'package:weblibre/features/web_feed/data/database/daos/feed.dart' as i4;
import 'package:drift/internal/modular.dart' as i5;
import 'package:sqlite3/common.dart' as i6;
abstract class $FeedDatabase extends i0.GeneratedDatabase {
$FeedDatabase(i0.QueryExecutor e) : super(e);
$FeedDatabaseManager get managers => $FeedDatabaseManager(this);
late final i1.Feed feed = i1.Feed(this);
late final i1.Article article = i1.Article(this);
late final i1.ArticleView articleView = i1.ArticleView(this);
late final i1.ArticleFts articleFts = i1.ArticleFts(this);
late final i2.ArticleDao articleDao = i2.ArticleDao(this as i3.FeedDatabase);
late final i4.FeedDao feedDao = i4.FeedDao(this as i3.FeedDatabase);
i1.DefinitionsDrift get definitionsDrift => i5.ReadDatabaseContainer(
this,
).accessor<i1.DefinitionsDrift>(i1.DefinitionsDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@override
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
feed,
article,
articleView,
i1.articleFeedId,
articleFts,
i1.articleAfterInsert,
i1.articleAfterDelete,
i1.articleAfterUpdate,
];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>
const i0.StreamQueryUpdateRules([
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'feed',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [i0.TableUpdate('article', kind: i0.UpdateKind.delete)],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'article',
limitUpdateKind: i0.UpdateKind.insert,
),
result: [i0.TableUpdate('article_fts', kind: i0.UpdateKind.insert)],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'article',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [i0.TableUpdate('article_fts', kind: i0.UpdateKind.insert)],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'article',
limitUpdateKind: i0.UpdateKind.update,
),
result: [i0.TableUpdate('article_fts', kind: i0.UpdateKind.insert)],
),
]);
}
class $FeedDatabaseManager {
final $FeedDatabase _db;
$FeedDatabaseManager(this._db);
i1.$FeedTableManager get feed => i1.$FeedTableManager(_db, _db.feed);
i1.$ArticleTableManager get article =>
i1.$ArticleTableManager(_db, _db.article);
i1.$ArticleFtsTableManager get articleFts =>
i1.$ArticleFtsTableManager(_db, _db.articleFts);
}
extension DefineFunctions on i6.CommonDatabase {
void defineFunctions({
required String Function(int, String?) lexoRankNext,
required String Function(int, String?) lexoRankPrevious,
required String Function(String?, String?) lexoRankReorderAfter,
required String Function(String?, String?) lexoRankReorderBefore,
}) {
createFunction(
functionName: 'lexo_rank_next',
argumentCount: const i6.AllowedArgumentCount(2),
function: (args) {
final arg0 = args[0] as int;
final arg1 = args[1] as String?;
return lexoRankNext(arg0, arg1);
},
);
createFunction(
functionName: 'lexo_rank_previous',
argumentCount: const i6.AllowedArgumentCount(2),
function: (args) {
final arg0 = args[0] as int;
final arg1 = args[1] as String?;
return lexoRankPrevious(arg0, arg1);
},
);
createFunction(
functionName: 'lexo_rank_reorder_after',
argumentCount: const i6.AllowedArgumentCount(2),
function: (args) {
final arg0 = args[0] as String?;
final arg1 = args[1] as String?;
return lexoRankReorderAfter(arg0, arg1);
},
);
createFunction(
functionName: 'lexo_rank_reorder_before',
argumentCount: const i6.AllowedArgumentCount(2),
function: (args) {
final arg0 = args[0] as String?;
final arg1 = args[1] as String?;
return lexoRankReorderBefore(arg0, arg1);
},
);
}
}
@@ -0,0 +1,137 @@
import 'package:weblibre/data/database/converters/uri.dart';
import 'package:weblibre/features/web_feed/data/models/feed_article_query_result.dart';
import 'package:weblibre/features/web_feed/data/database/converters/feed_authors.dart';
import 'package:weblibre/features/web_feed/data/database/converters/feed_categories.dart';
import 'package:weblibre/features/web_feed/data/database/converters/feed_links.dart';
import 'package:weblibre/features/web_feed/data/models/feed_article.dart';
CREATE TABLE feed (
url TEXT PRIMARY KEY NOT NULL MAPPED BY `const UriConverter()`,
title TEXT,
description TEXT,
icon TEXT MAPPED BY `const UriConverterNullable()`,
site_link TEXT MAPPED BY `const UriConverterNullable()`,
authors TEXT MAPPED BY `const FeedAuthorsConverter()`,
tags TEXT MAPPED BY `const FeedCategoriesConverter()`,
last_fetched DATETIME
);
CREATE TABLE article (
id TEXT PRIMARY KEY NOT NULL,
feed_id TEXT NOT NULL MAPPED BY `const UriConverter()` REFERENCES feed ("url") ON DELETE CASCADE,
fetched DATETIME NOT NULL,
created DATETIME,
updated DATETIME,
last_read DATETIME,
title TEXT,
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;
CREATE VIEW article_view WITH FeedArticle AS
SELECT
a.*,
f.icon,
f.site_link
FROM
article a
INNER JOIN
feed f on f.url = a.feed_id;
CREATE INDEX article_feed_id ON article (feed_id);
CREATE VIRTUAL TABLE article_fts
USING fts5(
title,
summaryPlain,
contentPlain,
content=article,
tokenize="trigram"
);
-- Triggers to keep the FTS index up to date.
CREATE TRIGGER article_after_insert AFTER INSERT ON article BEGIN
INSERT INTO
article_fts(rowid, title, summaryPlain, contentPlain)
VALUES (new.rowid, new.title, new.summaryPlain, new.contentPlain);
END;
CREATE TRIGGER article_after_delete AFTER DELETE ON article BEGIN
INSERT INTO
article_fts(article_fts, rowid, title, summaryPlain, contentPlain)
VALUES('delete', old.rowid, old.title, old.summaryPlain, old.contentPlain);
END;
CREATE TRIGGER article_after_update AFTER UPDATE ON article BEGIN
INSERT INTO
article_fts(article_fts, rowid, title, summaryPlain, contentPlain)
VALUES('delete', old.rowid, old.title, old.summaryPlain, old.contentPlain);
INSERT INTO
article_fts(rowid, title, summaryPlain, contentPlain)
VALUES (new.rowid, new.title, new.summaryPlain, new.contentPlain);
END;
optimizeFtsIndex:
INSERT INTO article_fts(article_fts) VALUES ('optimize');
queryArticlesBasic(:feed_id AS TEXT OR NULL) WITH FeedArticleQueryResult:
WITH weights AS (
SELECT
-- Customize these weights (higher = more important)
1.0 as title_weight -- Title matches are most important
)
SELECT
a.*,
f.icon,
(
bm25(article_fts, weights.title_weight)
) AS weighted_rank
FROM article_fts fts
INNER JOIN
article a ON a.rowid = fts.rowid
INNER JOIN
feed f ON f.url = a.feed_id
CROSS JOIN weights
WHERE
fts.title LIKE :query AND
(:feed_id IS NULL OR a.feed_id = :feed_id)
ORDER BY
weighted_rank ASC,
a.created DESC NULLS LAST
LIMIT :limit;
queryArticlesFullContent(:feed_id AS TEXT OR NULL) WITH FeedArticleQueryResult:
WITH weights AS (
SELECT
-- Customize these weights (higher = more important)
10.0 as title_weight, -- Title matches are most important
3.0 as summary_weight, -- Summary matches are quite important
1.0 as content_weight -- Content matches are basic
)
SELECT
a.*,
f.icon,
highlight(article_fts, 0, :beforeMatch, :afterMatch) AS title_highlight,
snippet(article_fts, 1, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS summary_snippet,
snippet(article_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS content_snippet,
(
bm25(article_fts, weights.title_weight, weights.summary_weight,
weights.content_weight)
) AS weighted_rank
FROM article_fts(:query) fts
INNER JOIN
article a ON a.rowid = fts.rowid
INNER JOIN
feed f ON f.url = a.feed_id
CROSS JOIN weights
WHERE
:feed_id IS NULL OR a.feed_id = :feed_id
ORDER BY
weighted_rank ASC,
a.created DESC NULLS LAST
LIMIT :limit;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,154 @@
/*
* 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: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';
import 'package:weblibre/features/web_feed/data/database/definitions.drift.dart';
import 'package:weblibre/features/web_feed/data/models/feed_author.dart';
import 'package:weblibre/features/web_feed/data/models/feed_category.dart';
import 'package:weblibre/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;
final DateTime fetched;
final DateTime? created;
final DateTime? updated;
final DateTime? lastRead;
final String? title;
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,
required this.feedId,
required this.fetched,
this.created,
this.updated,
this.lastRead,
this.title,
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) =>
_$FeedArticleFromJson(json);
Map<String, dynamic> toJson() => _$FeedArticleToJson(this);
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
{
map['feed_id'] = Variable<String>(Article.$converterfeedId.toSql(feedId));
}
map['fetched'] = Variable<DateTime>(fetched);
if (!nullToAbsent || created != null) {
map['created'] = Variable<DateTime>(created);
}
if (!nullToAbsent || updated != null) {
map['updated'] = Variable<DateTime>(updated);
}
if (!nullToAbsent || lastRead != null) {
map['last_read'] = Variable<DateTime>(lastRead);
}
if (!nullToAbsent || title != null) {
map['title'] = Variable<String>(title);
}
if (!nullToAbsent || authors != null) {
map['authors'] = Variable<String>(
Article.$converterauthorsn.toSql(authors),
);
}
if (!nullToAbsent || tags != null) {
map['tags'] = Variable<String>(Article.$convertertagsn.toSql(tags));
}
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);
}
if (!nullToAbsent || contentPlain != null) {
map['contentPlain'] = Variable<String>(contentPlain);
}
return map;
}
@override
List<Object?> get hashParameters => [
id,
feedId,
fetched,
created,
updated,
lastRead,
title,
authors,
tags,
links,
summaryHtml,
summaryMarkdown,
summaryPlain,
contentHtml,
contentMarkdown,
contentPlain,
icon,
siteLink,
];
}
@@ -0,0 +1,313 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
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);
FeedArticle siteLink(Uri? siteLink);
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `FeedArticle(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```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,
Uri? siteLink,
});
}
/// Callable proxy for `copyWith` functionality.
/// Use as `instanceOfFeedArticle.copyWith(...)` or call `instanceOfFeedArticle.copyWith.fieldName(value)` for a single field.
class _$FeedArticleCWProxyImpl implements _$FeedArticleCWProxy {
const _$FeedArticleCWProxyImpl(this._value);
final FeedArticle _value;
@override
FeedArticle id(String id) => call(id: id);
@override
FeedArticle feedId(Uri feedId) => call(feedId: feedId);
@override
FeedArticle fetched(DateTime fetched) => call(fetched: fetched);
@override
FeedArticle created(DateTime? created) => call(created: created);
@override
FeedArticle updated(DateTime? updated) => call(updated: updated);
@override
FeedArticle lastRead(DateTime? lastRead) => call(lastRead: lastRead);
@override
FeedArticle title(String? title) => call(title: title);
@override
FeedArticle authors(List<FeedAuthor>? authors) => call(authors: authors);
@override
FeedArticle tags(List<FeedCategory>? tags) => call(tags: tags);
@override
FeedArticle links(List<FeedLink>? links) => call(links: links);
@override
FeedArticle summaryHtml(String? summaryHtml) =>
call(summaryHtml: summaryHtml);
@override
FeedArticle summaryMarkdown(String? summaryMarkdown) =>
call(summaryMarkdown: summaryMarkdown);
@override
FeedArticle summaryPlain(String? summaryPlain) =>
call(summaryPlain: summaryPlain);
@override
FeedArticle contentHtml(String? contentHtml) =>
call(contentHtml: contentHtml);
@override
FeedArticle contentMarkdown(String? contentMarkdown) =>
call(contentMarkdown: contentMarkdown);
@override
FeedArticle contentPlain(String? contentPlain) =>
call(contentPlain: contentPlain);
@override
FeedArticle icon(Uri? icon) => call(icon: icon);
@override
FeedArticle siteLink(Uri? siteLink) => call(siteLink: siteLink);
@override
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `FeedArticle(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```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(),
Object? siteLink = const $CopyWithPlaceholder(),
}) {
return FeedArticle(
id: id == const $CopyWithPlaceholder() || id == null
? _value.id
// ignore: cast_nullable_to_non_nullable
: id as String,
feedId: feedId == const $CopyWithPlaceholder() || feedId == null
? _value.feedId
// ignore: cast_nullable_to_non_nullable
: feedId as Uri,
fetched: fetched == const $CopyWithPlaceholder() || fetched == null
? _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?,
siteLink: siteLink == const $CopyWithPlaceholder()
? _value.siteLink
// ignore: cast_nullable_to_non_nullable
: siteLink as Uri?,
);
}
}
extension $FeedArticleCopyWith on FeedArticle {
/// Returns a callable class used to build a new instance with modified fields.
/// Example: `instanceOfFeedArticle.copyWith(...)` or `instanceOfFeedArticle.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$FeedArticleCWProxy get copyWith => _$FeedArticleCWProxyImpl(this);
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FeedArticle _$FeedArticleFromJson(Map<String, dynamic> json) => FeedArticle(
id: json['id'] as String,
feedId: Uri.parse(json['feedId'] as String),
fetched: DateTime.parse(json['fetched'] as String),
created: json['created'] == null
? null
: DateTime.parse(json['created'] as String),
updated: json['updated'] == null
? null
: DateTime.parse(json['updated'] as String),
lastRead: json['lastRead'] == null
? null
: DateTime.parse(json['lastRead'] as String),
title: json['title'] as String?,
authors: (json['authors'] as List<dynamic>?)
?.map((e) => FeedAuthor.fromJson(e as Map<String, dynamic>))
.toList(),
tags: (json['tags'] as List<dynamic>?)
?.map((e) => FeedCategory.fromJson(e as Map<String, dynamic>))
.toList(),
links: (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),
siteLink: json['siteLink'] == null
? null
: Uri.parse(json['siteLink'] as String),
);
Map<String, dynamic> _$FeedArticleToJson(FeedArticle instance) =>
<String, dynamic>{
'id': instance.id,
'feedId': instance.feedId.toString(),
'fetched': instance.fetched.toIso8601String(),
'created': instance.created?.toIso8601String(),
'updated': instance.updated?.toIso8601String(),
'lastRead': instance.lastRead?.toIso8601String(),
'title': instance.title,
'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(),
'siteLink': instance.siteLink?.toString(),
};
@@ -0,0 +1,61 @@
/*
* 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:weblibre/features/web_feed/data/models/feed_article.dart';
class FeedArticleQueryResult extends FeedArticle {
final String? titleHighlight;
final String? summarySnippet;
final String? contentSnippet;
final double weightedRank;
FeedArticleQueryResult({
required super.id,
required super.feedId,
required super.fetched,
required this.weightedRank,
required super.created,
required super.updated,
required super.lastRead,
required super.title,
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,
this.titleHighlight,
this.summarySnippet,
this.contentSnippet,
});
@override
List<Object?> get hashParameters => [
...super.hashParameters,
weightedRank,
summarySnippet,
contentSnippet,
titleHighlight,
];
}
@@ -0,0 +1,39 @@
/*
* 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:fast_equatable/fast_equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'feed_author.g.dart';
@JsonSerializable()
class FeedAuthor with FastEquatable {
final String? name;
final String? email;
FeedAuthor({this.name, this.email});
factory FeedAuthor.fromJson(Map<String, dynamic> json) =>
_$FeedAuthorFromJson(json);
Map<String, dynamic> toJson() => _$FeedAuthorToJson(this);
@override
List<Object?> get hashParameters => [name, email];
}
@@ -0,0 +1,13 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_author.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FeedAuthor _$FeedAuthorFromJson(Map<String, dynamic> json) =>
FeedAuthor(name: json['name'] as String?, email: json['email'] as String?);
Map<String, dynamic> _$FeedAuthorToJson(FeedAuthor instance) =>
<String, dynamic>{'name': instance.name, 'email': instance.email};
@@ -0,0 +1,39 @@
/*
* 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:fast_equatable/fast_equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'feed_category.g.dart';
@JsonSerializable()
class FeedCategory with FastEquatable {
final String id;
final String? title;
FeedCategory({required this.id, this.title});
factory FeedCategory.fromJson(Map<String, dynamic> json) =>
_$FeedCategoryFromJson(json);
Map<String, dynamic> toJson() => _$FeedCategoryToJson(this);
@override
List<Object?> get hashParameters => [id, title];
}
@@ -0,0 +1,13 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_category.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FeedCategory _$FeedCategoryFromJson(Map<String, dynamic> json) =>
FeedCategory(id: json['id'] as String, title: json['title'] as String?);
Map<String, dynamic> _$FeedCategoryToJson(FeedCategory instance) =>
<String, dynamic>{'id': instance.id, 'title': instance.title};
@@ -0,0 +1,57 @@
/*
* 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:fast_equatable/fast_equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'feed_link.g.dart';
enum FeedLinkRelation {
///an alternate representation of the entry or feed, for example a permalink to the html version of the entry, or the front page of the weblog.
alternate,
///a related resource which is potentially large in size and might require special handling, for example an audio or video recording.
enclosure,
///an document related to the entry or feed.
related,
///the feed itself.
self,
///the source of the information provided in the entry.
via,
}
@JsonSerializable()
class FeedLink with FastEquatable {
final Uri uri;
final FeedLinkRelation? relation;
final String? title;
FeedLink({required this.uri, this.relation, this.title});
factory FeedLink.fromJson(Map<String, dynamic> json) =>
_$FeedLinkFromJson(json);
Map<String, dynamic> toJson() => _$FeedLinkToJson(this);
@override
List<Object?> get hashParameters => [uri, relation, title];
}
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_link.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FeedLink _$FeedLinkFromJson(Map<String, dynamic> json) => FeedLink(
uri: Uri.parse(json['uri'] as String),
relation: $enumDecodeNullable(_$FeedLinkRelationEnumMap, json['relation']),
title: json['title'] as String?,
);
Map<String, dynamic> _$FeedLinkToJson(FeedLink instance) => <String, dynamic>{
'uri': instance.uri.toString(),
'relation': _$FeedLinkRelationEnumMap[instance.relation],
'title': instance.title,
};
const _$FeedLinkRelationEnumMap = {
FeedLinkRelation.alternate: 'alternate',
FeedLinkRelation.enclosure: 'enclosure',
FeedLinkRelation.related: 'related',
FeedLinkRelation.self: 'self',
FeedLinkRelation.via: 'via',
};
@@ -0,0 +1,39 @@
/*
* 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:json_annotation/json_annotation.dart';
import 'package:weblibre/features/web_feed/data/database/converters/feed_data.dart';
import 'package:weblibre/features/web_feed/data/database/definitions.drift.dart';
import 'package:weblibre/features/web_feed/data/models/feed_article.dart';
part 'feed_parse_result.g.dart';
@JsonSerializable()
class FeedParseResult {
@FeedDataConverter()
final FeedData feedData;
final List<FeedArticle> articleData;
FeedParseResult({required this.feedData, required this.articleData});
factory FeedParseResult.fromJson(Map<String, dynamic> json) =>
_$FeedParseResultFromJson(json);
Map<String, dynamic> toJson() => _$FeedParseResultToJson(this);
}
@@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_parse_result.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FeedParseResult _$FeedParseResultFromJson(Map<String, dynamic> json) =>
FeedParseResult(
feedData: const FeedDataConverter().fromJson(
json['feedData'] as Map<String, dynamic>,
),
articleData: (json['articleData'] as List<dynamic>)
.map((e) => FeedArticle.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$FeedParseResultToJson(FeedParseResult instance) =>
<String, dynamic>{
'feedData': const FeedDataConverter().toJson(instance.feedData),
'articleData': instance.articleData.map((e) => e.toJson()).toList(),
};
@@ -0,0 +1,55 @@
/*
* 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:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'package:weblibre/core/database_registry.dart';
import 'package:weblibre/core/filesystem.dart';
import 'package:weblibre/features/web_feed/data/database/database.dart';
part 'providers.g.dart';
@Riverpod(keepAlive: true)
FeedDatabase feedDatabase(Ref ref) {
final db = FeedDatabase(
LazyDatabase(() async {
final file = File(p.join(filesystem.profileDatabasesDir.path, 'feed.db'));
// Also work around limitations on old Android versions
if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
return NativeDatabase.createInBackground(file);
}),
);
DatabaseRegistry.instance.register('feed', db);
ref.onDispose(() async {
await db.close();
});
return db;
}
@@ -0,0 +1,51 @@
// 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(feedDatabase)
final feedDatabaseProvider = FeedDatabaseProvider._();
final class FeedDatabaseProvider
extends $FunctionalProvider<FeedDatabase, FeedDatabase, FeedDatabase>
with $Provider<FeedDatabase> {
FeedDatabaseProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'feedDatabaseProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$feedDatabaseHash();
@$internal
@override
$ProviderElement<FeedDatabase> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
FeedDatabase create(Ref ref) {
return feedDatabase(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(FeedDatabase value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<FeedDatabase>(value),
);
}
}
String _$feedDatabaseHash() => r'1ecae87a3de5b2d43136fdb73411727b0b217621';