feat: search, filter and act on items from the library pages

This commit is contained in:
randogoth 2026-10-11 13:25:57 +03:00
parent 1b426ccd8b
commit 5cd053c8a9
11 changed files with 401 additions and 43 deletions

View file

@ -162,26 +162,72 @@ class MewsDatabase extends _$MewsDatabase {
return (await query.getSingle()).read(total) ?? 0;
}
/// The most recent external visits, newest first.
Future<List<HistoryItem>> recentVisits({int limit = 50}) async {
final query = (select(historyItems)
..orderBy([
(r) => OrderingTerm.desc(r.visitedAt),
(r) => OrderingTerm.desc(r.id),
])
..limit(limit));
return query.get();
/// The most recently visited pages, newest first, one row per address so a
/// page visited often is not listed over and over. [query] narrows by title
/// or address.
Future<List<VisitedPage>> recentPages({String? query, int limit = 200}) async {
// Group on the highest row id rather than the latest timestamp: visits
// recorded within the same clock tick share a timestamp, and sqlite then
// picks any one of their rows for the plain columns below. Ids always
// climb, so the newest visit is the one with the largest id.
final lastVisit = historyItems.id.max();
final statement = selectOnly(historyItems)
..addColumns([
historyItems.url,
historyItems.title,
historyItems.visitedAt,
lastVisit,
])
..groupBy([historyItems.url])
..orderBy([OrderingTerm.desc(lastVisit)])
..limit(limit);
final filter = _search([historyItems.url, historyItems.title], query);
if (filter != null) statement.where(filter);
return (await statement.get()).map((row) {
return VisitedPage(
// Selecting a plain column alongside max() gives the value from the
// row that matched it, so these describe the latest visit.
url: row.read(historyItems.url)!,
title: row.read(historyItems.title),
visitedAt: row.read(historyItems.visitedAt)!,
);
}).toList();
}
/// Bookmarks in the order they were added, newest first.
Future<List<Bookmark>> bookmarksByDate() async {
final query = (select(bookmarks)
/// Removes every visit to one page.
Future<void> forgetPage(String url) =>
(delete(historyItems)..where((r) => r.url.equals(url))).go();
Future<void> clearHistory() => delete(historyItems).go();
/// Narrows a list to rows where any of [columns] contains [query]. Returns
/// null when there is nothing to search for, so the caller adds no clause.
Expression<bool>? _search(
List<GeneratedColumn<String>> columns, String? query) {
final needle = query?.trim();
if (needle == null || needle.isEmpty) return null;
// The reader is typing text, not a pattern, so LIKE's wildcards are
// escaped and matched literally.
final pattern =
'%${needle.replaceAll('\\', '\\\\').replaceAll('%', '\\%').replaceAll('_', '\\_')}%';
return columns
.map((column) => column.like(pattern, escapeChar: '\\'))
.reduce((a, b) => a | b);
}
/// Bookmarks in the order they were added, newest first. [query] narrows by
/// title or address.
Future<List<Bookmark>> bookmarksByDate({String? query}) async {
final statement = (select(bookmarks)
..orderBy([
(r) => OrderingTerm.desc(r.createdAt),
// Tiebreak bookmarks added within the same clock tick.
(r) => OrderingTerm.asc(r.url),
]));
return query.get();
final filter = _search([bookmarks.url, bookmarks.title], query);
if (filter != null) statement.where((_) => filter);
return statement.get();
}
// Bookmarks -----------------------------------------------------------
@ -214,13 +260,16 @@ class MewsDatabase extends _$MewsDatabase {
await _removeTagsOf(TagKind.subscription, url);
});
Future<List<Subscription>> allSubscriptions() async {
final query = (select(subscriptions)
/// Subscribed sites, newest first. [query] narrows by title or address.
Future<List<Subscription>> allSubscriptions({String? query}) async {
final statement = (select(subscriptions)
..orderBy([
(r) => OrderingTerm.desc(r.createdAt),
(r) => OrderingTerm.asc(r.url),
]));
return query.get();
final filter = _search([subscriptions.url, subscriptions.title], query);
if (filter != null) statement.where((_) => filter);
return statement.get();
}
/// Records that a site was checked, whether or not anything was found.
@ -432,6 +481,19 @@ enum EntrySort {
};
}
/// One page the reader visited, collapsed to its latest visit.
class VisitedPage {
const VisitedPage({
required this.url,
required this.title,
required this.visitedAt,
});
final String url;
final String? title;
final DateTime visitedAt;
}
/// One post together with the site that published it.
class RiverEntry {
const RiverEntry({