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

@ -76,7 +76,9 @@ class _MewsPageViewState extends State<MewsPageView> {
textDirection: widget.page.direction == TextDirection.rtl
? ui.TextDirection.rtl
: ui.TextDirection.ltr,
child: ColoredBox(
// Page text is the reader's to copy; forms manage their own selection.
child: SelectionArea(
child: ColoredBox(
color: palette.background,
child: ListView(
controller: widget.scrollController,
@ -94,6 +96,7 @@ class _MewsPageViewState extends State<MewsPageView> {
),
],
),
),
),
);
}

View file

@ -116,7 +116,7 @@ const _monoDark = MewsPalette(
/// Resolves the palette: the reader's variant choice overrides the page's own
/// variant unless the choice is "site" (spec 5.3, 8.2).
MewsPalette paletteFor({required MewsVariant pageVariant, required String readerVariant, required bool dark}) {
final variant = readerVariant == 'site' ? pageVariant.name : pageVariantFromName(readerVariant);
final variant = readerVariant == 'site' ? pageVariant : pageVariantFromName(readerVariant);
return switch ((variant, dark)) {
(MewsVariant.warm, false) => _warmLight,
(MewsVariant.warm, true) => _warmDark,

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart' show SignalBuilder;
import '../../application/controllers/reader_settings_controller.dart';
import '../../application/internal_pages.dart';
import '../app.dart';
import 'reader_fonts.dart';
@ -44,6 +45,24 @@ class SettingsSheet extends StatelessWidget {
style: Theme.of(context).textTheme.titleMedium,
),
),
// The library pages are internal Mews pages; opening one joins
// the walk history like any other page.
ListTile(
leading: const Icon(Icons.history),
title: Text(strings.t('settings-history')),
onTap: () => _openPage(context, InternalPages.historyUrl),
),
ListTile(
leading: const Icon(Icons.star_outline),
title: Text(strings.t('settings-bookmarks')),
onTap: () => _openPage(context, InternalPages.bookmarksUrl),
),
ListTile(
leading: const Icon(Icons.notifications_outlined),
title: Text(strings.t('settings-subscriptions')),
onTap: () => _openPage(context, InternalPages.subscriptionsUrl),
),
const Divider(),
_heading(context, strings.t('settings-font-heading')),
RadioGroup<String>(
groupValue: settings.font.value,
@ -125,7 +144,7 @@ class SettingsSheet extends StatelessWidget {
),
),
const Divider(),
if (url != null) ...[
if (url != null && !InternalPages.isInternal(url)) ...[
ListTile(
leading: Icon(library.isBookmarked(url) ? Icons.star : Icons.star_border),
title: Text(
@ -156,6 +175,11 @@ class SettingsSheet extends StatelessWidget {
});
}
void _openPage(BuildContext context, String url) {
Navigator.of(context).pop();
AppScope.of(context).deps.browser.open(url);
}
Widget _heading(BuildContext context, String text) => Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: Text(

View file

@ -73,6 +73,10 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
bool get _settledAnimating => _settling != null && _settle.isAnimating;
/// How close to a screen edge a drag must begin to navigate; horizontal
/// drags elsewhere belong to text selection.
static const _edgeZone = 40.0;
void _onDragStart(DragStartDetails details) {
if (_settledAnimating) return;
setState(() => _dragging = true);
@ -161,24 +165,37 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
)
: null;
return GestureDetector(
behavior: HitTestBehavior.deferToChild,
onHorizontalDragStart: _onDragStart,
onHorizontalDragUpdate: _onDragUpdate,
onHorizontalDragEnd: _onDragEnd,
child: Stack(
children: [
if (peek != null) Positioned.fill(child: peek),
Transform.translate(
offset: Offset(dx, 0),
child: widget.child,
),
],
),
return Stack(
children: [
if (peek != null) Positioned.fill(child: peek),
Transform.translate(
offset: Offset(dx, 0),
child: widget.child,
),
// Only horizontal drags that begin at a screen edge navigate;
// the strips are translucent, so taps, vertical scrolling and
// text selection still pass through everywhere.
_edgeStrip(fromLeft: true),
_edgeStrip(fromLeft: false),
],
);
},
);
}
Widget _edgeStrip({required bool fromLeft}) => Positioned(
left: fromLeft ? 0 : null,
right: fromLeft ? null : 0,
top: 0,
bottom: 0,
width: _edgeZone,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onHorizontalDragStart: _onDragStart,
onHorizontalDragUpdate: _onDragUpdate,
onHorizontalDragEnd: _onDragEnd,
),
);
}
/// The neighboring page's fallback panel while it is still being parsed.