feat: add quotations from marked text, with citations and sharing
This commit is contained in:
parent
0e2027c9e2
commit
e6e84fdc01
25 changed files with 1558 additions and 4 deletions
|
|
@ -148,6 +148,21 @@ class SiteCertificates extends Table {
|
|||
Set<Column> get primaryKey => {host, port};
|
||||
}
|
||||
|
||||
/// A stretch of text the reader marked on a page and kept, together with the
|
||||
/// page it came from. The author is carried only when the page's metadata
|
||||
/// said one, and the site's title when the reader's subscriptions know the
|
||||
/// site the page belongs to. The column is named [Quotations.quote] because
|
||||
/// `text` is the drift builder the declaration would otherwise call.
|
||||
class Quotations extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get pageUrl => text()();
|
||||
TextColumn get pageTitle => text().nullable()();
|
||||
TextColumn get author => text().nullable()();
|
||||
TextColumn get siteTitle => text().nullable()();
|
||||
TextColumn get quote => text()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
@DriftDatabase(tables: [
|
||||
HistoryItems,
|
||||
Bookmarks,
|
||||
|
|
@ -160,6 +175,7 @@ class SiteCertificates extends Table {
|
|||
ClientCertificates,
|
||||
SiteCertificates,
|
||||
FirstStart,
|
||||
Quotations,
|
||||
])
|
||||
class MewsDatabase extends _$MewsDatabase {
|
||||
MewsDatabase() : super(_open());
|
||||
|
|
@ -168,7 +184,7 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
MewsDatabase.test(super.executor);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 6;
|
||||
int get schemaVersion => 8;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
|
|
@ -197,6 +213,12 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
if (from < 6) {
|
||||
await m.createTable(firstStart);
|
||||
}
|
||||
if (from < 7) {
|
||||
await m.createTable(quotations);
|
||||
}
|
||||
if (from < 8) {
|
||||
await m.addColumn(quotations, quotations.siteTitle);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -717,6 +739,87 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
return [for (final row in rows) '${row.host}:${row.port}'];
|
||||
}
|
||||
|
||||
// Quotations --------------------------------------------------------------
|
||||
|
||||
/// Keeps a marked stretch of text together with the page it came from,
|
||||
/// and with the site's title when the reader's subscriptions know the site
|
||||
/// the page belongs to: the site whose posts listed the page, or the page
|
||||
/// itself when it is the subscribed one.
|
||||
Future<void> addQuotation({
|
||||
required String quote,
|
||||
required String pageUrl,
|
||||
String? pageTitle,
|
||||
String? author,
|
||||
}) async {
|
||||
into(quotations).insert(QuotationsCompanion.insert(
|
||||
quote: quote,
|
||||
pageUrl: pageUrl,
|
||||
pageTitle: Value(pageTitle),
|
||||
author: Value(author),
|
||||
siteTitle: Value(await siteTitleOf(pageUrl)),
|
||||
));
|
||||
}
|
||||
|
||||
/// The title of the site [pageUrl] belongs to, or null when the reader's
|
||||
/// subscriptions do not know it. A page the site lists as a post takes the
|
||||
/// subscribing site's title; a subscribed page is its own site.
|
||||
Future<String?> siteTitleOf(String pageUrl) async {
|
||||
final site = alias(subscriptions, 'site');
|
||||
final throughPosts = await ((select(feedEntries).join([
|
||||
leftOuterJoin(site, site.url.equalsExp(feedEntries.siteUrl)),
|
||||
]))
|
||||
..where(feedEntries.url.equals(pageUrl)))
|
||||
.get();
|
||||
if (throughPosts.isNotEmpty) {
|
||||
return throughPosts.first.readTableOrNull(site)?.title;
|
||||
}
|
||||
final own =
|
||||
await (select(subscriptions)..where((r) => r.url.equals(pageUrl)))
|
||||
.getSingleOrNull();
|
||||
return own?.title;
|
||||
}
|
||||
|
||||
Future<void> removeQuotation(int id) =>
|
||||
(delete(quotations)..where((q) => q.id.equals(id))).go();
|
||||
|
||||
/// One quotation, so an action address can act on it.
|
||||
Future<Quotation?> quotationById(int id) =>
|
||||
(select(quotations)..where((q) => q.id.equals(id))).getSingleOrNull();
|
||||
|
||||
/// Every kept quotation, for the quotations page. [query] narrows by the
|
||||
/// quoted text, the page's title, its author or its address.
|
||||
Future<List<Quotation>> quotationsList({
|
||||
QuotationSort sort = QuotationSort.date,
|
||||
String? query,
|
||||
}) async {
|
||||
final statement = select(quotations)
|
||||
..orderBy(switch (sort) {
|
||||
QuotationSort.date => [
|
||||
// Tiebreak quotations saved within the same clock tick.
|
||||
(r) => OrderingTerm.desc(r.createdAt),
|
||||
(r) => OrderingTerm.asc(r.id),
|
||||
],
|
||||
QuotationSort.page => [
|
||||
// A page without a title sorts under its address, so the order
|
||||
// never depends on a null.
|
||||
(r) => OrderingTerm.asc(coalesce([r.pageTitle, r.pageUrl])),
|
||||
(r) => OrderingTerm.desc(r.createdAt),
|
||||
],
|
||||
});
|
||||
final filter = _search(
|
||||
[quotations.quote, quotations.pageTitle, quotations.author, quotations.pageUrl],
|
||||
query);
|
||||
if (filter != null) statement.where((_) => filter);
|
||||
return statement.get();
|
||||
}
|
||||
|
||||
/// How many quotations are held, for the hub.
|
||||
Future<int> countQuotations() async {
|
||||
final total = quotations.id.count();
|
||||
final query = selectOnly(quotations)..addColumns([total]);
|
||||
return (await query.getSingle()).read(total) ?? 0;
|
||||
}
|
||||
|
||||
/// Binds a certificate to a host, replacing whatever was bound before.
|
||||
Future<void> bindCertificate(String host, int port, int certificateId) =>
|
||||
into(siteCertificates).insert(
|
||||
|
|
@ -743,6 +846,17 @@ enum EntrySort {
|
|||
};
|
||||
}
|
||||
|
||||
/// How the quotations page is ordered.
|
||||
enum QuotationSort {
|
||||
date,
|
||||
page;
|
||||
|
||||
/// Reads the order from an address, falling back to the default rather than
|
||||
/// refusing a value the reader may have typed.
|
||||
static QuotationSort parse(String? name) =>
|
||||
name == 'page' ? QuotationSort.page : QuotationSort.date;
|
||||
}
|
||||
|
||||
/// How widely one tag is used.
|
||||
class TagUse {
|
||||
const TagUse({required this.bookmarks, required this.sites});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue