feat: lay our own pages out as interface rather than as articles
This commit is contained in:
parent
e7a8e91334
commit
dfba1cfb16
8 changed files with 162 additions and 55 deletions
|
|
@ -20,6 +20,7 @@ class MewsPageView extends StatefulWidget {
|
|||
required this.onLinkTap,
|
||||
required this.onFormSubmit,
|
||||
this.scrollController,
|
||||
this.ownPage = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -32,6 +33,12 @@ class MewsPageView extends StatefulWidget {
|
|||
final void Function(FormBlock form, Map<String, String> values) onFormSubmit;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
/// Whether this is one of the client's own pages. Those are built from the
|
||||
/// same blocks, but they are interface rather than an article, so they are
|
||||
/// laid out denser and wider, rows are divided instead of bulleted, and
|
||||
/// links read as controls rather than prose.
|
||||
final bool ownPage;
|
||||
|
||||
@override
|
||||
State<MewsPageView> createState() => _MewsPageViewState();
|
||||
}
|
||||
|
|
@ -86,8 +93,10 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
// max-width: 40em
|
||||
constraints: BoxConstraints(maxWidth: 40 * base),
|
||||
// max-width: 40em for an article; our own pages are lists, so
|
||||
// they get more room before they stop growing.
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: (widget.ownPage ? 56 : 40) * base),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: blocks,
|
||||
|
|
@ -105,7 +114,11 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
padding: EdgeInsets.only(bottom: base),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(color: palette.foreground.withValues(alpha: 0.7), fontSize: base * 0.85),
|
||||
style: TextStyle(
|
||||
color: palette.foreground.withValues(alpha: 0.7),
|
||||
fontFamily: widget.fontFamily,
|
||||
fontSize: base * 0.85,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -113,12 +126,19 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
|
||||
Widget _block(BuildContext context, MewsBlock block, double base) {
|
||||
final palette = widget.palette;
|
||||
final foreground = TextStyle(color: palette.foreground, fontSize: base, height: 1.5);
|
||||
final foreground = TextStyle(
|
||||
color: palette.foreground,
|
||||
fontFamily: widget.fontFamily,
|
||||
fontSize: base,
|
||||
height: 1.5,
|
||||
);
|
||||
|
||||
return switch (block) {
|
||||
HeadingBlock(:final level, :final inline) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: level == 1 ? base * 0.5 : base * 1.6,
|
||||
top: level == 1
|
||||
? base * 0.5
|
||||
: (widget.ownPage ? base : base * 1.6),
|
||||
bottom: base * 0.5,
|
||||
),
|
||||
child: Text.rich(
|
||||
|
|
@ -127,11 +147,16 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
fontWeight: FontWeight.bold,
|
||||
height: 1.25,
|
||||
fontSize: base * const [1.6, 1.3, 1.1, 1.0, 1.0, 1.0][level - 1],
|
||||
// A section label on our own pages, not a document heading.
|
||||
color: widget.ownPage && level > 1
|
||||
? palette.foreground.withValues(alpha: 0.7)
|
||||
: null,
|
||||
letterSpacing: widget.ownPage && level > 1 ? 0.4 : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
ParagraphBlock(:final inline) => Padding(
|
||||
padding: EdgeInsets.only(bottom: base),
|
||||
padding: EdgeInsets.only(bottom: widget.ownPage ? base * 0.35 : base),
|
||||
child: Text.rich(TextSpan(children: _inline(inline)), style: foreground),
|
||||
),
|
||||
AddressBlock(:final inline) => Padding(
|
||||
|
|
@ -168,9 +193,33 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
),
|
||||
),
|
||||
RuleBlock() => Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: base * 2),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: widget.ownPage ? base * 0.6 : base * 2),
|
||||
child: Divider(color: palette.border, height: 1),
|
||||
),
|
||||
// Rows on our own pages: separated by a line rather than marked with a
|
||||
// bullet, so a list of bookmarks or settings reads as a list of rows.
|
||||
ListBlock(:final items) when widget.ownPage => Padding(
|
||||
padding: EdgeInsets.only(bottom: base * 0.35),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (var i = 0; i < items.length; i++) ...[
|
||||
if (i > 0) Divider(color: palette.border, height: 1),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: base * 0.45),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (final child in items[i].blocks)
|
||||
_block(context, child, base),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
ListBlock(:final ordered, :final start, :final items) => Padding(
|
||||
padding: EdgeInsets.only(bottom: base),
|
||||
child: Column(
|
||||
|
|
@ -274,6 +323,7 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
form: block,
|
||||
foreground: foreground,
|
||||
onFormSubmit: widget.onFormSubmit,
|
||||
ownPage: widget.ownPage,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
|
@ -311,6 +361,10 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
MewsEmphasis(:final style, :final children) => TextSpan(
|
||||
style: switch (style) {
|
||||
EmphasisStyle.strong => const TextStyle(fontWeight: FontWeight.bold),
|
||||
EmphasisStyle.emphasis when widget.ownPage => TextStyle(
|
||||
color: palette.foreground.withValues(alpha: 0.65),
|
||||
fontSize: 15.0 * widget.fontScale,
|
||||
),
|
||||
EmphasisStyle.emphasis => const TextStyle(fontStyle: FontStyle.italic),
|
||||
EmphasisStyle.term => const TextStyle(fontStyle: FontStyle.italic),
|
||||
EmphasisStyle.quote => const TextStyle(fontStyle: FontStyle.italic),
|
||||
|
|
@ -320,7 +374,11 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
MewsLink(:final href, :final children) => TextSpan(
|
||||
style: TextStyle(
|
||||
color: widget.visitedUrls.contains(href) ? palette.visited : palette.link,
|
||||
decoration: TextDecoration.underline,
|
||||
// Underlines mark links in prose; on our own pages they are
|
||||
// controls, so weight carries them instead.
|
||||
decoration:
|
||||
widget.ownPage ? TextDecoration.none : TextDecoration.underline,
|
||||
fontWeight: widget.ownPage ? FontWeight.w500 : null,
|
||||
),
|
||||
recognizer: _recognizer(node),
|
||||
children: _inline(children, _recognizer(node))),
|
||||
|
|
@ -342,12 +400,17 @@ class _MewsForm extends StatefulWidget {
|
|||
required this.form,
|
||||
required this.foreground,
|
||||
required this.onFormSubmit,
|
||||
this.ownPage = false,
|
||||
});
|
||||
|
||||
final FormBlock form;
|
||||
final TextStyle foreground;
|
||||
final void Function(FormBlock form, Map<String, String> values) onFormSubmit;
|
||||
|
||||
/// On our own pages a form is a search box or a one-field prompt, so it sits
|
||||
/// on one line instead of filling the column like a page's own form.
|
||||
final bool ownPage;
|
||||
|
||||
@override
|
||||
State<_MewsForm> createState() => _MewsFormState();
|
||||
}
|
||||
|
|
@ -370,6 +433,38 @@ class _MewsFormState extends State<_MewsForm> {
|
|||
final form = widget.form;
|
||||
final style = widget.foreground;
|
||||
|
||||
Widget button(MewsSubmitField field) => FilledButton(
|
||||
onPressed: () => field.reset ? _reset(form) : _submit(form),
|
||||
child: Text(
|
||||
field.label ??
|
||||
(field.reset
|
||||
? strings.t('form-reset-default')
|
||||
: strings.t('form-submit-default')),
|
||||
style: TextStyle(fontFamily: style.fontFamily),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.ownPage) {
|
||||
final inputs = form.fields.where((f) => f is! MewsSubmitField).toList();
|
||||
return Form(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
for (final field in inputs)
|
||||
if (field is! MewsHiddenField)
|
||||
Expanded(child: _field(field, style))
|
||||
else
|
||||
_field(field, style),
|
||||
for (final field in form.fields.whereType<MewsSubmitField>())
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 8),
|
||||
child: button(field),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Form(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
|
@ -378,13 +473,7 @@ class _MewsFormState extends State<_MewsForm> {
|
|||
for (final field in form.fields.whereType<MewsSubmitField>())
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: FilledButton(
|
||||
onPressed: () => field.reset ? _reset(form) : _submit(form),
|
||||
child: Text(
|
||||
field.label ??
|
||||
(field.reset ? strings.t('form-reset-default') : strings.t('form-submit-default')),
|
||||
),
|
||||
),
|
||||
child: button(field),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:signals/signals_flutter.dart' show SignalBuilder;
|
|||
|
||||
import '../../application/controllers/browser_controller.dart'
|
||||
show BrokenPage, LoadFailed;
|
||||
import '../../application/pages/mews_urls.dart';
|
||||
import '../../mews/document.dart';
|
||||
import '../../mews/parser.dart' show mewsPlainText;
|
||||
import '../app.dart';
|
||||
|
|
@ -159,6 +160,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
|||
return MewsPageView(
|
||||
key: ValueKey(page.url),
|
||||
page: page,
|
||||
ownPage: MewsUrls.isInternal(page.url),
|
||||
palette: palette,
|
||||
fontFamily: font,
|
||||
fontScale: fontScale,
|
||||
|
|
@ -186,6 +188,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
|||
color: palette.background,
|
||||
child: MewsPageView(
|
||||
page: page,
|
||||
ownPage: MewsUrls.isInternal(page.url),
|
||||
palette: palette,
|
||||
fontFamily: settings.font.value,
|
||||
fontScale: settings.fontScale.value,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue