feat: offer a non-Mews web page as a simplified view or in the browser

This commit is contained in:
randogoth 2026-10-12 11:13:34 +03:00
parent cea66fbb42
commit 00e42c925c
24 changed files with 863 additions and 152 deletions

View file

@ -22,7 +22,6 @@ import '../../application/controllers/browser_controller.dart'
import '../../database/database.dart' show ClientCertificate;
import '../../application/pages/mews_urls.dart';
import '../../mews/document.dart';
import '../../mews/parser.dart' show mewsPlainText;
import '../app.dart';
import 'address_bar.dart';
import 'mews_page_view.dart';
@ -46,7 +45,11 @@ class _ReaderScreenState extends State<ReaderScreen> {
/// scrolls down, back on any upward scroll. A pointer host with a keyboard
/// has room to spare and keys to reach the bar, so it never hides.
bool _chromeVisible = true;
bool _showBrokenText = false;
/// The address whose simplified view the reader asked for. Keyed by address
/// so the choice applies to that page alone: a link from it that also fails
/// explains itself rather than silently simplifying too.
String? _simplifiedFor;
final _checkedSubscriptions = <String>{};
// The finger behind a possible tap on the page, tracked outside the
@ -274,7 +277,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
return SignalBuilder(builder: (context) {
final error = deps.browser.error.value;
return switch (error) {
BrokenPage(:final source) => _errorPage(context, source),
final BrokenPage broken => _errorPage(context, broken),
LoadFailed() => _errorPage(context, null),
// A protocol status carries the site's own words for what went
// wrong.
@ -300,7 +303,15 @@ class _ReaderScreenState extends State<ReaderScreen> {
}
_checkSubscription(context, page);
_showBrokenText = false;
return _pageView(context, page);
}
/// A page as the reader sees it. Both the fetched page and the simplified
/// view of a web page come through here, so they share the palette, the
/// fonts, the link handling and the choice to keep a quotation.
Widget _pageView(BuildContext context, MewsPage page) {
final deps = AppScope.of(context).deps;
return SignalBuilder(builder: (context) {
final settings = deps.settings;
@ -536,20 +547,28 @@ class _ReaderScreenState extends State<ReaderScreen> {
}
}
Widget _errorPage(BuildContext context, String? brokenSource) {
/// The page that stands in for one the reader cannot have: [broken] is a
/// page the Mews parse refused, and null is a page that would not load.
Widget _errorPage(BuildContext context, BrokenPage? broken) {
final strings = AppScope.of(context).strings;
final deps = AppScope.of(context).deps;
// A web address can be shown by the system's browser, so a page that
// will not load here can leave; a protocol page has nowhere else to go,
// so only trying again is offered there.
// will not load here, or that this client cannot read, can leave; a
// protocol page has nowhere else to go, so only trying again is offered
// there.
final failed = deps.browser.currentAddress;
final failedScheme = Uri.tryParse(failed)?.scheme;
final canOpenOutside =
brokenSource == null && (failedScheme == 'http' || failedScheme == 'https');
final canOpenOutside = failedScheme == 'http' || failedScheme == 'https';
// The simplified view is a page, not a notice, so it fills the screen
// rather than sitting in the centered column below.
if (broken?.simplified != null && _simplifiedFor == failed) {
return _pageView(context, broken!.simplified!);
}
Widget content;
if (brokenSource == null) {
if (broken == null) {
content = Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -568,25 +587,33 @@ class _ReaderScreenState extends State<ReaderScreen> {
],
],
);
} else if (_showBrokenText) {
content = Column(
children: [
Text(strings.t('error-broken-page-text-title'),
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Expanded(child: SingleChildScrollView(child: Text(mewsPlainText(brokenSource)))),
],
);
} else {
// A page claiming the Mews marker that still will not parse has a real
// markup error. One that never claimed it is an ordinary web page, and
// saying so is the honest answer; the MIME type cannot tell them apart,
// since Mews pages are served as text/html (spec 7.1).
content = Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(strings.t('error-broken-page'), textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton(
onPressed: _press(() => setState(() => _showBrokenText = true)),
child: Text(strings.t('error-broken-page-show-text')),
Text(
strings.t(broken.declaresMews
? 'error-broken-page'
: 'error-not-mews-page'),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
if (broken.simplified != null)
FilledButton(
onPressed: _press(() => setState(() => _simplifiedFor = failed)),
child: Text(strings.t('error-show-simplified')),
),
if (canOpenOutside) ...[
const SizedBox(height: 8),
OutlinedButton(
onPressed: _press(() => _openOutside(failed)),
child: Text(strings.t('error-open-in-browser')),
),
],
],
);
}