feat: color a site from its address, and offer a failed web page to the system browser

This commit is contained in:
randogoth 2026-10-11 22:32:44 +03:00
parent 56db479df0
commit 9d9f09ebc7
24 changed files with 597 additions and 25 deletions

View file

@ -1,5 +1,8 @@
import 'dart:io' show Process;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, TargetPlatform;
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ScrollDirection;
import 'package:flutter/services.dart' show LogicalKeyboardKey;
@ -114,6 +117,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
pageVariant: page.variant,
readerVariant: deps.settings.variant.value,
dark: Theme.of(context).brightness == Brightness.dark,
url: page.url,
).background;
// The 40em column plus its padding: swipes are accepted
@ -235,6 +239,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
pageVariant: page.variant,
readerVariant: variant,
dark: Theme.of(context).brightness == Brightness.dark,
url: page.url,
);
return MewsPageView(
key: ValueKey(page.url),
@ -263,6 +268,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
pageVariant: page.variant,
readerVariant: settings.variant.value,
dark: Theme.of(context).brightness == Brightness.dark,
url: page.url,
);
return ColoredBox(
color: palette.background,
@ -389,10 +395,46 @@ class _ReaderScreenState extends State<ReaderScreen> {
);
}
/// Hands [url] to the system's web browser. The reader does not watch
/// what happens to it there, and a system with no browser answers nothing
/// rather than failing here.
///
/// The launcher asks GIO on Linux, which can fail to see the session's
/// browser from the environment the app runs in, and a binary built before
/// the launcher was added answers not at all; in both cases xdg-open asks
/// the session directly.
Future<void> _openOutside(String url) async {
final uri = Uri.tryParse(url);
if (uri == null) return;
var opened = false;
try {
opened = await launchUrl(uri, mode: LaunchMode.externalApplication);
} on Exception {
// No answer; the fallback below asks the session itself.
}
if (opened) return;
if (defaultTargetPlatform == TargetPlatform.linux) {
final result = await Process.run('xdg-open', [url]);
// A missing xdg-open or a session without a browser answers with an
// error; nothing more to do but stay where we are.
if (result.exitCode != 0) return;
}
}
Widget _errorPage(BuildContext context, String? brokenSource) {
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.
final failed = deps.browser.currentAddress;
final failedScheme = Uri.tryParse(failed)?.scheme;
final canOpenOutside =
brokenSource == null && (failedScheme == 'http' || failedScheme == 'https');
Widget content;
if (brokenSource == null) {
content = Column(
@ -404,6 +446,13 @@ class _ReaderScreenState extends State<ReaderScreen> {
onPressed: () => deps.browser.reload(),
child: Text(strings.t('error-try-again')),
),
if (canOpenOutside) ...[
const SizedBox(height: 8),
OutlinedButton(
onPressed: () => _openOutside(failed),
child: Text(strings.t('error-open-in-browser')),
),
],
],
);
} else if (_showBrokenText) {