feat: tag bookmarks and sites, and browse the library by tag
This commit is contained in:
parent
5cd053c8a9
commit
b0b86ef5b1
13 changed files with 445 additions and 21 deletions
|
|
@ -218,7 +218,7 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
|
||||
/// Bookmarks in the order they were added, newest first. [query] narrows by
|
||||
/// title or address.
|
||||
Future<List<Bookmark>> bookmarksByDate({String? query}) async {
|
||||
Future<List<Bookmark>> bookmarksByDate({String? query, String? tag}) async {
|
||||
final statement = (select(bookmarks)
|
||||
..orderBy([
|
||||
(r) => OrderingTerm.desc(r.createdAt),
|
||||
|
|
@ -227,6 +227,9 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
]));
|
||||
final filter = _search([bookmarks.url, bookmarks.title], query);
|
||||
if (filter != null) statement.where((_) => filter);
|
||||
if (tag != null && tag.isNotEmpty) {
|
||||
statement.where((r) => r.url.isInQuery(_taggedUrls(TagKind.bookmark, tag)));
|
||||
}
|
||||
return statement.get();
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +264,7 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
});
|
||||
|
||||
/// Subscribed sites, newest first. [query] narrows by title or address.
|
||||
Future<List<Subscription>> allSubscriptions({String? query}) async {
|
||||
Future<List<Subscription>> allSubscriptions({String? query, String? tag}) async {
|
||||
final statement = (select(subscriptions)
|
||||
..orderBy([
|
||||
(r) => OrderingTerm.desc(r.createdAt),
|
||||
|
|
@ -269,6 +272,10 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
]));
|
||||
final filter = _search([subscriptions.url, subscriptions.title], query);
|
||||
if (filter != null) statement.where((_) => filter);
|
||||
if (tag != null && tag.isNotEmpty) {
|
||||
statement
|
||||
.where((r) => r.url.isInQuery(_taggedUrls(TagKind.subscription, tag)));
|
||||
}
|
||||
return statement.get();
|
||||
}
|
||||
|
||||
|
|
@ -405,6 +412,75 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
|
||||
// Tags ---------------------------------------------------------------------
|
||||
|
||||
/// The addresses of one kind carrying [tag], for filtering a library page.
|
||||
JoinedSelectStatement<ItemTags, ItemTag> _taggedUrls(
|
||||
TagKind kind, String tag) {
|
||||
return selectOnly(itemTags)
|
||||
..addColumns([itemTags.url])
|
||||
..where(itemTags.kind.equalsValue(kind) & itemTags.tag.equals(tag));
|
||||
}
|
||||
|
||||
/// Every tag in use with how many bookmarks and sites carry it. A tag exists
|
||||
/// only while something carries it, so this is the whole list of tags.
|
||||
Future<Map<String, TagUse>> tagCounts() async {
|
||||
final total = itemTags.url.count();
|
||||
final query = selectOnly(itemTags)
|
||||
..addColumns([itemTags.tag, itemTags.kind, total])
|
||||
..groupBy([itemTags.tag, itemTags.kind])
|
||||
..orderBy([OrderingTerm.asc(itemTags.tag)]);
|
||||
|
||||
final counts = <String, TagUse>{};
|
||||
for (final row in await query.get()) {
|
||||
final tag = row.read(itemTags.tag)!;
|
||||
final kind = itemTags.kind.converter.fromSql(row.read(itemTags.kind)!);
|
||||
final count = row.read(total) ?? 0;
|
||||
final held = counts[tag] ?? const TagUse(bookmarks: 0, sites: 0);
|
||||
counts[tag] = switch (kind) {
|
||||
TagKind.bookmark => TagUse(bookmarks: count, sites: held.sites),
|
||||
TagKind.subscription =>
|
||||
TagUse(bookmarks: held.bookmarks, sites: count),
|
||||
};
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/// The tags on several items at once, so a list of rows costs one query.
|
||||
Future<Map<String, List<String>>> tagsOfMany(
|
||||
TagKind kind, List<String> urls) async {
|
||||
if (urls.isEmpty) return const {};
|
||||
final rows = await (select(itemTags)
|
||||
..where((r) => r.kind.equalsValue(kind) & r.url.isIn(urls))
|
||||
..orderBy([
|
||||
(r) => OrderingTerm.asc(r.createdAt),
|
||||
(r) => OrderingTerm.asc(r.tag),
|
||||
]))
|
||||
.get();
|
||||
final tags = <String, List<String>>{};
|
||||
for (final row in rows) {
|
||||
(tags[row.url] ??= []).add(row.tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
/// Removes a tag from everything carrying it.
|
||||
Future<void> deleteTag(String tag) =>
|
||||
(delete(itemTags)..where((r) => r.tag.equals(tag))).go();
|
||||
|
||||
/// What the reader calls one bookmark or site, for titling its tags page.
|
||||
Future<String?> itemTitle(TagKind kind, String url) async {
|
||||
switch (kind) {
|
||||
case TagKind.bookmark:
|
||||
final row = await (select(bookmarks)..where((r) => r.url.equals(url)))
|
||||
.getSingleOrNull();
|
||||
return row?.title;
|
||||
case TagKind.subscription:
|
||||
final row =
|
||||
await (select(subscriptions)..where((r) => r.url.equals(url)))
|
||||
.getSingleOrNull();
|
||||
return row?.title;
|
||||
}
|
||||
}
|
||||
|
||||
/// The tags on one item, in the order they were added.
|
||||
Future<List<String>> tagsOf(TagKind kind, String url) async {
|
||||
final query = (select(itemTags)
|
||||
|
|
@ -481,6 +557,16 @@ enum EntrySort {
|
|||
};
|
||||
}
|
||||
|
||||
/// How widely one tag is used.
|
||||
class TagUse {
|
||||
const TagUse({required this.bookmarks, required this.sites});
|
||||
|
||||
final int bookmarks;
|
||||
final int sites;
|
||||
|
||||
int get total => bookmarks + sites;
|
||||
}
|
||||
|
||||
/// One page the reader visited, collapsed to its latest visit.
|
||||
class VisitedPage {
|
||||
const VisitedPage({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue