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
|
|
@ -5,7 +5,9 @@ import 'package:signals/signals.dart';
|
|||
import '../../database/database.dart';
|
||||
import '../../mews/document.dart';
|
||||
import '../actors/fetch_actor.dart';
|
||||
import '../internal_actions.dart';
|
||||
import '../internal_pages.dart';
|
||||
import '../pages/mews_urls.dart';
|
||||
|
||||
/// What went wrong while browsing. The presentation layer maps these to
|
||||
/// user-facing messages; no raw codes cross into the UI.
|
||||
|
|
@ -32,13 +34,16 @@ class BrowserController {
|
|||
required MewsDatabase db,
|
||||
required Actor<FetchCommand, FetchResult> fetch,
|
||||
InternalPages? internalPages,
|
||||
InternalActions? internalActions,
|
||||
}) : _db = db,
|
||||
_fetch = fetch,
|
||||
_internalPages = internalPages;
|
||||
_internalPages = internalPages,
|
||||
_internalActions = internalActions;
|
||||
|
||||
final MewsDatabase _db;
|
||||
final Actor<FetchCommand, FetchResult> _fetch;
|
||||
final InternalPages? _internalPages;
|
||||
final InternalActions? _internalActions;
|
||||
|
||||
/// The stack of visited pages; the reader is at [_stackIndex].
|
||||
final List<String> _stack = [];
|
||||
|
|
@ -82,14 +87,18 @@ class BrowserController {
|
|||
}
|
||||
|
||||
Future<void> open(String input) async {
|
||||
final url = normalizeAddress(input);
|
||||
final url = MewsUrls.canonical(normalizeAddress(input));
|
||||
if (url.isEmpty) return;
|
||||
|
||||
// Setting links change a value and stay on the current page instead of
|
||||
// Action links change something and stay on the current page instead of
|
||||
// joining the walk history.
|
||||
if (InternalPages.isSettingUrl(url)) {
|
||||
await _internalPages?.applySetting(url);
|
||||
await _loadCurrent();
|
||||
if (MewsUrls.isAction(url)) {
|
||||
// Only the client's own pages may change the reader's data. A fetched
|
||||
// page linking to mews:do/... must not act on their behalf (spec 8.1).
|
||||
if (!_onInternalPage) return;
|
||||
if (await (_internalActions?.apply(url) ?? Future.value(false))) {
|
||||
await _loadCurrent();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -145,10 +154,39 @@ class BrowserController {
|
|||
|
||||
Future<void> reload() => _loadCurrent();
|
||||
|
||||
/// Whether the reader is standing on one of the client's own pages.
|
||||
bool get _onInternalPage =>
|
||||
_stackIndex >= 0 && MewsUrls.isInternal(_stack[_stackIndex]);
|
||||
|
||||
/// Rebuilds the open page when it is one of ours, so a change made elsewhere
|
||||
/// shows up without the reader navigating.
|
||||
Future<void> refreshInternalPage() async {
|
||||
if (!_onInternalPage) return;
|
||||
await _loadCurrent();
|
||||
}
|
||||
|
||||
Future<void> submitForm(String action, String method, Map<String, String> fields) async {
|
||||
// A form on one of our own pages is a navigation or an action, never a
|
||||
// network request; the fetch client cannot speak the mews scheme.
|
||||
if (MewsUrls.isInternal(action)) {
|
||||
final base = Uri.parse(action);
|
||||
final verb = MewsUrls.verbOf(action);
|
||||
return open(verb == null
|
||||
? MewsUrls.page(base, fields)
|
||||
: MewsUrls.action(verb, {...base.queryParameters, ...fields}));
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
final result = await _fetch.send(SubmitForm(url: action, method: method, fields: fields));
|
||||
final FetchResult result;
|
||||
try {
|
||||
result = await _fetch.send(SubmitForm(url: action, method: method, fields: fields));
|
||||
} catch (_) {
|
||||
// A submit that throws out of the actor is still just a failed load.
|
||||
loading.value = false;
|
||||
error.value = const LoadFailed();
|
||||
return;
|
||||
}
|
||||
loading.value = false;
|
||||
switch (result) {
|
||||
case FetchFormOk(:final page):
|
||||
|
|
@ -162,10 +200,10 @@ class BrowserController {
|
|||
}
|
||||
}
|
||||
|
||||
/// Reopens the most recent page from the previous session, if any.
|
||||
/// Reopens the most recent page from the previous session, or the hub when
|
||||
/// there is nothing to return to.
|
||||
Future<void> restoreLastSession() async {
|
||||
final url = await _db.lastVisitedUrl();
|
||||
if (url == null) return;
|
||||
final url = await _db.lastVisitedUrl() ?? MewsUrls.hub;
|
||||
_stack.add(url);
|
||||
_stackIndex = 0;
|
||||
_updateNeighbors();
|
||||
|
|
@ -216,7 +254,8 @@ class BrowserController {
|
|||
if (page != null) {
|
||||
currentPage.value = page;
|
||||
error.value = null;
|
||||
visitedUrls.value = {...visitedUrls.value, url};
|
||||
// Our own addresses stay out of visitedUrls: a sort or filter link
|
||||
// would otherwise take the visited color after one use.
|
||||
} else {
|
||||
currentPage.value = null;
|
||||
error.value = const LoadFailed();
|
||||
|
|
|
|||
|
|
@ -16,10 +16,16 @@ class ReaderSettingsController {
|
|||
static const maxScale = 2.0;
|
||||
static const scaleStep = 0.125;
|
||||
|
||||
/// The accepted values, in the order the settings page offers them. A
|
||||
/// setting arrives as a link the reader can also type, so a value outside
|
||||
/// these lists is refused rather than stored.
|
||||
static const themeModes = ['light', 'dark', 'system'];
|
||||
static const variants = ['site', 'warm', 'cool', 'green', 'mono'];
|
||||
|
||||
final font = signal('Atkinson Hyperlegible');
|
||||
final fontScale = signal(1.0);
|
||||
final themeMode = signal('system'); // system | light | dark
|
||||
final variant = signal('site'); // site | warm | cool | green | mono
|
||||
final themeMode = signal('system');
|
||||
final variant = signal('site');
|
||||
|
||||
Future<void> load() async {
|
||||
final settings = await _db.readSettings();
|
||||
|
|
@ -30,28 +36,37 @@ class ReaderSettingsController {
|
|||
variant.value = settings.variant;
|
||||
}
|
||||
|
||||
Future<void> setFont(String family) async {
|
||||
Future<bool> setFont(String family) async {
|
||||
if (family.isEmpty || family == font.value) return false;
|
||||
font.value = family;
|
||||
await _persist();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> decreaseSize() => _setScale(fontScale.value - scaleStep);
|
||||
Future<void> increaseSize() => _setScale(fontScale.value + scaleStep);
|
||||
Future<bool> decreaseSize() => _setScale(fontScale.value - scaleStep);
|
||||
Future<bool> increaseSize() => _setScale(fontScale.value + scaleStep);
|
||||
|
||||
Future<void> _setScale(double value) async {
|
||||
if (value < minScale || value > maxScale) return;
|
||||
Future<bool> _setScale(double value) async {
|
||||
if (value < minScale || value > maxScale) return false;
|
||||
fontScale.value = value;
|
||||
await _persist();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(String mode) async {
|
||||
Future<bool> setThemeMode(String mode) async {
|
||||
if (!themeModes.contains(mode) || mode == themeMode.value) return false;
|
||||
themeMode.value = mode;
|
||||
await _persist();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> setVariant(String variant) async {
|
||||
Future<bool> setVariant(String variant) async {
|
||||
if (!variants.contains(variant) || variant == this.variant.value) {
|
||||
return false;
|
||||
}
|
||||
this.variant.value = variant;
|
||||
await _persist();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _persist() => _db.saveSettings(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue