feat: add a hub for the client's own pages and gate mews:do action links
This commit is contained in:
parent
df705d831f
commit
b311b9f7dd
21 changed files with 966 additions and 404 deletions
144
app/lib/application/pages/page_blocks.dart
Normal file
144
app/lib/application/pages/page_blocks.dart
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/// The shared vocabulary every internal page is built from. Internal pages are
|
||||
/// ordinary Mews documents, so a page has only headings, lists, paragraphs and
|
||||
/// links to work with; these builders keep the result consistent.
|
||||
library;
|
||||
|
||||
import '../../l10n/strings.dart';
|
||||
import '../../mews/document.dart';
|
||||
import 'mews_urls.dart';
|
||||
|
||||
/// Separates independent labels on one line. A separator is layout, so it is a
|
||||
/// constant here rather than a message.
|
||||
const actionSeparator = ' ';
|
||||
|
||||
/// One choice in a group of mutually exclusive values.
|
||||
typedef Choice = ({String href, String label, bool current});
|
||||
|
||||
/// One entry in the strip of internal pages.
|
||||
typedef NavItem = ({String url, String labelId});
|
||||
|
||||
/// The strip carried by every internal page, in a fixed order so its position
|
||||
/// never shifts under the reader.
|
||||
const navItems = <NavItem>[
|
||||
(url: MewsUrls.hub, labelId: 'nav-hub'),
|
||||
(url: MewsUrls.bookmarks, labelId: 'nav-bookmarks'),
|
||||
(url: MewsUrls.subscriptions, labelId: 'nav-subscriptions'),
|
||||
(url: MewsUrls.history, labelId: 'nav-history'),
|
||||
(url: MewsUrls.settings, labelId: 'nav-settings'),
|
||||
];
|
||||
|
||||
/// The title and the navigation strip that open every internal page. The page
|
||||
/// the reader is on is plain text, matching how a current value is marked.
|
||||
List<MewsBlock> chrome({
|
||||
required String title,
|
||||
required Uri current,
|
||||
required Strings strings,
|
||||
}) {
|
||||
final inline = <MewsInline>[];
|
||||
for (final item in navItems) {
|
||||
if (inline.isNotEmpty) inline.add(const MewsText(actionSeparator));
|
||||
final label = strings.t(item.labelId);
|
||||
// Compare paths so a filtered or sorted page still marks its own entry.
|
||||
if (Uri.parse(item.url).path == current.path) {
|
||||
inline.add(MewsEmphasis(EmphasisStyle.strong, [MewsText(label)]));
|
||||
} else {
|
||||
inline.add(MewsLink(item.url, [MewsText(label)]));
|
||||
}
|
||||
}
|
||||
return [
|
||||
HeadingBlock(1, [MewsText(title)]),
|
||||
ParagraphBlock(inline),
|
||||
const RuleBlock(),
|
||||
];
|
||||
}
|
||||
|
||||
/// One row in a list: the item, then an optional note on its own line. A date
|
||||
/// goes inside the link, the way a Mews page writes its own dated links (spec
|
||||
/// 6.1), so the whole row is one target. One paragraph per row keeps rows
|
||||
/// close together, since each paragraph carries its own bottom padding.
|
||||
MewsListItem itemRow({
|
||||
required String href,
|
||||
required String label,
|
||||
String? date,
|
||||
String? note,
|
||||
}) {
|
||||
return MewsListItem([
|
||||
ParagraphBlock([
|
||||
MewsLink(href, [MewsText(date == null ? label : '$date $label')]),
|
||||
if (note != null) ...[
|
||||
const MewsLineBreak(),
|
||||
MewsEmphasis(EmphasisStyle.emphasis, [MewsText(note)]),
|
||||
],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/// A group of mutually exclusive values: the current one plain, the rest links.
|
||||
ListBlock choiceRow(List<Choice> options) {
|
||||
return ListBlock(
|
||||
ordered: false,
|
||||
start: 1,
|
||||
items: [
|
||||
for (final option in options)
|
||||
MewsListItem([
|
||||
ParagraphBlock([
|
||||
if (option.current)
|
||||
MewsEmphasis(EmphasisStyle.strong, [MewsText(option.label)])
|
||||
else
|
||||
MewsLink(option.href, [MewsText(option.label)]),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// A plain list of links, used for menus and library lists alike.
|
||||
ListBlock itemList(List<MewsListItem> items) =>
|
||||
ListBlock(ordered: false, start: 1, items: items);
|
||||
|
||||
/// Wraps blocks as an internal document. The marker makes the renderer treat
|
||||
/// it as a Mews page rather than prepending the "not a Mews page" notice, and
|
||||
/// no entries are offered so an internal page is never subscribable.
|
||||
MewsPage internalPage({
|
||||
required Uri url,
|
||||
required String title,
|
||||
required List<MewsBlock> body,
|
||||
}) {
|
||||
return MewsPage(
|
||||
url: url.toString(),
|
||||
specVersion: 'internal',
|
||||
title: title,
|
||||
body: body,
|
||||
lang: 'en',
|
||||
direction: TextDirection.ltr,
|
||||
variant: MewsVariant.plain,
|
||||
entries: const [],
|
||||
);
|
||||
}
|
||||
|
||||
/// Picks the message for [count]. Plurals are separate messages, so the
|
||||
/// choice happens here and Fluent formats the number.
|
||||
String countText(
|
||||
Strings strings,
|
||||
int count, {
|
||||
required String none,
|
||||
required String one,
|
||||
required String many,
|
||||
}) {
|
||||
return switch (count) {
|
||||
0 => strings.t(none),
|
||||
1 => strings.t(one),
|
||||
_ => strings.t(many, {'count': count}),
|
||||
};
|
||||
}
|
||||
|
||||
/// YYYY-MM-DD, the shape a Mews entry link uses (spec 6.1).
|
||||
String dayText(DateTime date) =>
|
||||
'${date.year.toString().padLeft(4, '0')}-${_two(date.month)}-${_two(date.day)}';
|
||||
|
||||
String clockText(DateTime date) => '${_two(date.hour)}:${_two(date.minute)}';
|
||||
|
||||
/// The day and the time of day, for history rows.
|
||||
String dayTimeText(DateTime date) => '${dayText(date)} ${clockText(date)}';
|
||||
|
||||
String _two(int value) => value.toString().padLeft(2, '0');
|
||||
Loading…
Add table
Add a link
Reference in a new issue