feat: search, filter and act on items from the library pages
This commit is contained in:
parent
1b426ccd8b
commit
5cd053c8a9
11 changed files with 401 additions and 43 deletions
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue