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
|
|
@ -1,6 +1,7 @@
|
|||
import '../../database/database.dart';
|
||||
import '../../l10n/strings.dart';
|
||||
import '../../mews/document.dart';
|
||||
import 'mews_urls.dart';
|
||||
import 'page_blocks.dart';
|
||||
|
||||
Future<MewsPage> buildBookmarksPage(
|
||||
|
|
@ -8,7 +9,8 @@ Future<MewsPage> buildBookmarksPage(
|
|||
required MewsDatabase db,
|
||||
required Strings strings,
|
||||
}) async {
|
||||
final bookmarks = await db.bookmarksByDate();
|
||||
final query = url.queryParameters['q'] ?? '';
|
||||
final bookmarks = await db.bookmarksByDate(query: query);
|
||||
final title = strings.t('bookmarks-title');
|
||||
|
||||
return internalPage(
|
||||
|
|
@ -16,8 +18,22 @@ Future<MewsPage> buildBookmarksPage(
|
|||
title: title,
|
||||
body: [
|
||||
...chrome(title: title, current: url, strings: strings),
|
||||
searchForm(
|
||||
action: MewsUrls.bookmarks,
|
||||
label: strings.t('bookmarks-search-label'),
|
||||
submitLabel: strings.t('action-search'),
|
||||
query: query,
|
||||
),
|
||||
if (query.isNotEmpty)
|
||||
ParagraphBlock([
|
||||
MewsLink(MewsUrls.page(url, {'q': null}),
|
||||
[MewsText(strings.t('action-clear-search'))]),
|
||||
]),
|
||||
if (bookmarks.isEmpty)
|
||||
ParagraphBlock([MewsText(strings.t('bookmarks-empty'))])
|
||||
ParagraphBlock([
|
||||
MewsText(strings.t(
|
||||
query.isEmpty ? 'bookmarks-empty' : 'bookmarks-no-matches')),
|
||||
])
|
||||
else
|
||||
itemList([
|
||||
for (final bookmark in bookmarks)
|
||||
|
|
@ -25,6 +41,11 @@ Future<MewsPage> buildBookmarksPage(
|
|||
href: bookmark.url,
|
||||
label: bookmark.title ?? bookmark.url,
|
||||
date: dayText(bookmark.createdAt),
|
||||
actions: [
|
||||
action(
|
||||
MewsUrls.action('remove-bookmark', {'url': bookmark.url}),
|
||||
strings.t('action-remove-bookmark')),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import '../../database/database.dart';
|
||||
import '../../l10n/strings.dart';
|
||||
import '../../mews/document.dart';
|
||||
import 'mews_urls.dart';
|
||||
import 'page_blocks.dart';
|
||||
|
||||
Future<MewsPage> buildHistoryPage(
|
||||
|
|
@ -8,7 +9,9 @@ Future<MewsPage> buildHistoryPage(
|
|||
required MewsDatabase db,
|
||||
required Strings strings,
|
||||
}) async {
|
||||
final visits = await db.recentVisits();
|
||||
final query = url.queryParameters['q'] ?? '';
|
||||
final confirming = url.queryParameters['confirm'] == 'clear';
|
||||
final pages = await db.recentPages(query: query);
|
||||
final title = strings.t('history-title');
|
||||
|
||||
return internalPage(
|
||||
|
|
@ -16,15 +19,49 @@ Future<MewsPage> buildHistoryPage(
|
|||
title: title,
|
||||
body: [
|
||||
...chrome(title: title, current: url, strings: strings),
|
||||
if (visits.isEmpty)
|
||||
ParagraphBlock([MewsText(strings.t('history-empty'))])
|
||||
searchForm(
|
||||
action: MewsUrls.history,
|
||||
label: strings.t('history-search-label'),
|
||||
submitLabel: strings.t('action-search'),
|
||||
query: query,
|
||||
),
|
||||
// Clearing history cannot be undone, so it takes a second step rather
|
||||
// than a dialog: another page the reader can simply leave.
|
||||
if (confirming) ...[
|
||||
ParagraphBlock([MewsText(strings.t('history-clear-confirm'))]),
|
||||
ParagraphBlock([
|
||||
MewsLink(MewsUrls.action('clear-history'),
|
||||
[MewsText(strings.t('history-clear-yes'))]),
|
||||
const MewsText(actionSeparator),
|
||||
MewsLink(MewsUrls.history, [MewsText(strings.t('history-clear-no'))]),
|
||||
]),
|
||||
] else
|
||||
ParagraphBlock([
|
||||
if (query.isNotEmpty) ...[
|
||||
MewsLink(MewsUrls.page(url, {'q': null}),
|
||||
[MewsText(strings.t('action-clear-search'))]),
|
||||
const MewsText(actionSeparator),
|
||||
],
|
||||
if (pages.isNotEmpty)
|
||||
MewsLink(MewsUrls.page(url, {'confirm': 'clear'}),
|
||||
[MewsText(strings.t('history-clear'))]),
|
||||
]),
|
||||
if (pages.isEmpty)
|
||||
ParagraphBlock([
|
||||
MewsText(strings
|
||||
.t(query.isEmpty ? 'history-empty' : 'history-no-matches')),
|
||||
])
|
||||
else
|
||||
itemList([
|
||||
for (final visit in visits)
|
||||
for (final page in pages)
|
||||
itemRow(
|
||||
href: visit.url,
|
||||
label: visit.title ?? visit.url,
|
||||
date: dayTimeText(visit.visitedAt),
|
||||
href: page.url,
|
||||
label: page.title ?? page.url,
|
||||
date: dayTimeText(page.visitedAt),
|
||||
actions: [
|
||||
action(MewsUrls.action('forget-page', {'url': page.url}),
|
||||
strings.t('history-forget')),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -139,6 +139,33 @@ ListBlock choiceRow(List<Choice> options) {
|
|||
);
|
||||
}
|
||||
|
||||
/// A one-field search box that submits back to its own page, turning what the
|
||||
/// reader typed into a query string. [keep] carries the parameters the submit
|
||||
/// must not drop as hidden fields, the way an HTML form does.
|
||||
///
|
||||
/// Keep this at a fixed position in a page's blocks: the renderer matches form
|
||||
/// state by position, so a block appearing above it would reset what the
|
||||
/// reader typed.
|
||||
FormBlock searchForm({
|
||||
required String action,
|
||||
required String label,
|
||||
required String submitLabel,
|
||||
required String query,
|
||||
Map<String, String> keep = const {},
|
||||
}) {
|
||||
return FormBlock(
|
||||
action: action,
|
||||
method: 'get',
|
||||
submitLabel: submitLabel,
|
||||
fields: [
|
||||
MewsTextField(name: 'q', label: label, password: false, value: query),
|
||||
for (final kept in keep.entries)
|
||||
MewsHiddenField(name: kept.key, value: kept.value),
|
||||
MewsSubmitField(reset: false, label: submitLabel),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// A plain list of links, used for menus and library lists alike.
|
||||
ListBlock itemList(List<MewsListItem> items) =>
|
||||
ListBlock(ordered: false, start: 1, items: items);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import '../../database/database.dart';
|
||||
import '../../l10n/strings.dart';
|
||||
import '../../mews/document.dart';
|
||||
import 'mews_urls.dart';
|
||||
import 'page_blocks.dart';
|
||||
|
||||
Future<MewsPage> buildSubscriptionsPage(
|
||||
|
|
@ -8,7 +9,8 @@ Future<MewsPage> buildSubscriptionsPage(
|
|||
required MewsDatabase db,
|
||||
required Strings strings,
|
||||
}) async {
|
||||
final sites = await db.allSubscriptions();
|
||||
final query = url.queryParameters['q'] ?? '';
|
||||
final sites = await db.allSubscriptions(query: query);
|
||||
final unread = await db.unreadCounts();
|
||||
final title = strings.t('subscriptions-title');
|
||||
|
||||
|
|
@ -17,28 +19,53 @@ Future<MewsPage> buildSubscriptionsPage(
|
|||
title: title,
|
||||
body: [
|
||||
...chrome(title: title, current: url, strings: strings),
|
||||
searchForm(
|
||||
action: MewsUrls.subscriptions,
|
||||
label: strings.t('subscriptions-search-label'),
|
||||
submitLabel: strings.t('action-search'),
|
||||
query: query,
|
||||
),
|
||||
if (query.isNotEmpty)
|
||||
ParagraphBlock([
|
||||
MewsLink(MewsUrls.page(url, {'q': null}),
|
||||
[MewsText(strings.t('action-clear-search'))]),
|
||||
]),
|
||||
if (sites.isEmpty)
|
||||
ParagraphBlock([MewsText(strings.t('subscriptions-empty'))])
|
||||
ParagraphBlock([
|
||||
MewsText(strings.t(query.isEmpty
|
||||
? 'subscriptions-empty'
|
||||
: 'subscriptions-no-matches')),
|
||||
])
|
||||
else
|
||||
itemList([
|
||||
for (final site in sites)
|
||||
itemRow(
|
||||
href: site.url,
|
||||
label: site.title ?? site.url,
|
||||
date: dayText(site.createdAt),
|
||||
note: _unreadNote(strings, unread[site.url] ?? 0),
|
||||
note: _checkedNote(strings, site, unread[site.url] ?? 0),
|
||||
actions: [
|
||||
action(
|
||||
MewsUrls.action('refresh-subscription', {'url': site.url}),
|
||||
strings.t('action-refresh')),
|
||||
action(MewsUrls.action('unsubscribe', {'url': site.url}),
|
||||
strings.t('action-unsubscribe')),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The unread count for a site, or null when it has nothing unread, so the
|
||||
/// row carries no second line.
|
||||
String? _unreadNote(Strings strings, int count) {
|
||||
if (count == 0) return null;
|
||||
return countText(strings, count,
|
||||
none: 'count-unread-none',
|
||||
one: 'count-unread-one',
|
||||
many: 'count-unread-many');
|
||||
/// What the reader needs to know about a site: what is waiting, or when it was
|
||||
/// last looked at. Each is a whole message of its own.
|
||||
String _checkedNote(Strings strings, Subscription site, int unread) {
|
||||
if (unread > 0) {
|
||||
return countText(strings, unread,
|
||||
none: 'count-unread-none',
|
||||
one: 'count-unread-one',
|
||||
many: 'count-unread-many');
|
||||
}
|
||||
final checkedAt = site.lastCheckedAt;
|
||||
if (checkedAt == null) return strings.t('subscriptions-never-checked');
|
||||
return strings.t('subscriptions-checked', {'date': dayText(checkedAt)});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue