feat: theme chrome by palette, selectable text, wap fallback, and library pages

This commit is contained in:
randogoth 2026-10-11 11:48:24 +03:00
parent c104c2026b
commit 56f3d959fa
12 changed files with 534 additions and 51 deletions

View file

@ -5,6 +5,7 @@ import 'package:signals/signals.dart';
import '../../database/database.dart';
import '../../mews/document.dart';
import '../actors/fetch_actor.dart';
import '../internal_pages.dart';
/// What went wrong while browsing. The presentation layer maps these to
/// user-facing messages; no raw codes cross into the UI.
@ -27,12 +28,17 @@ class BrokenPage extends BrowseError {
/// dispatches fetch commands to the fetch actor. Local-first: a cached page is
/// revalidated with the server but survives any network failure.
class BrowserController {
BrowserController({required MewsDatabase db, required Actor<FetchCommand, FetchResult> fetch})
: _db = db,
_fetch = fetch;
BrowserController({
required MewsDatabase db,
required Actor<FetchCommand, FetchResult> fetch,
InternalPages? internalPages,
}) : _db = db,
_fetch = fetch,
_internalPages = internalPages;
final MewsDatabase _db;
final Actor<FetchCommand, FetchResult> _fetch;
final InternalPages? _internalPages;
/// The stack of visited pages; the reader is at [_stackIndex].
final List<String> _stack = [];
@ -54,14 +60,27 @@ class BrowserController {
bool get canGoForward => _stackIndex >= 0 && _stack.length - 1 > _stackIndex;
/// Normalizes what the reader typed into an address: bare domains become
/// https URLs.
/// https URLs; anything already carrying a scheme (mews:, https:, ...) is
/// left alone.
static String normalizeAddress(String input) {
final trimmed = input.trim();
if (trimmed.isEmpty) return '';
if (trimmed.contains('://')) return trimmed;
if (_schemePattern.hasMatch(trimmed)) return trimmed;
return 'https://$trimmed';
}
static final _schemePattern = RegExp(r'^[a-zA-Z][a-zA-Z0-9+.-]*:');
/// The wap. subdomain equivalent of [url], or null when the host already
/// starts with it or the address is not a plain web URL.
static String? wapUrlOf(String url) {
final uri = Uri.tryParse(url);
if (uri == null || !uri.hasScheme || uri.host.isEmpty) return null;
if (uri.scheme != 'http' && uri.scheme != 'https') return null;
if (uri.host.startsWith('wap.')) return null;
return uri.replace(host: 'wap.${uri.host}').toString();
}
Future<void> open(String input) async {
final url = normalizeAddress(input);
if (url.isEmpty) return;
@ -70,6 +89,23 @@ class BrowserController {
_stackIndex++;
_updateNeighbors();
await _loadCurrent();
// Mews sites commonly live on a wap. subdomain; try that before showing
// an error when the address as typed does not work out.
if (error.value != null && _stack[_stackIndex] == url) {
final wap = wapUrlOf(url);
if (wap != null) {
final firstError = error.value;
_stack[_stackIndex] = wap;
_updateNeighbors();
await _loadCurrent();
// The address as typed is the closer match; keep its error when the
// wap. attempt does not work either.
if (error.value != null && firstError != null) {
error.value = firstError;
}
}
}
}
Future<void> back() async {
@ -142,6 +178,10 @@ class BrowserController {
return;
}
final url = _stack[index];
if (_internalPages?.handles(url) ?? false) {
target.value = await _internalPages!.pageFor(url);
return;
}
final cached = await _db.cachedPage(url);
if (_stackIndex != expectedIndex) return;
if (cached == null) {
@ -159,6 +199,22 @@ class BrowserController {
loading.value = true;
error.value = null;
// Internal pages are built from the local database, never fetched or
// cached; they join the stack like any other page.
if (_internalPages?.handles(url) ?? false) {
final page = await _internalPages!.pageFor(url);
loading.value = false;
if (page != null) {
currentPage.value = page;
error.value = null;
visitedUrls.value = {...visitedUrls.value, url};
} else {
currentPage.value = null;
error.value = const LoadFailed();
}
return;
}
final cached = await _db.cachedPage(url);
final result = await _fetch.send(FetchPage(
url: url,