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

@ -0,0 +1,165 @@
import '../../database/database.dart';
import '../../l10n/strings.dart';
import '../../mews/document.dart';
import 'controllers/reader_settings_controller.dart'
show LibraryController;
/// Builds the internal History, Bookmarks and Subscriptions pages as real
/// [MewsPage]s: they render like any other page and take part in the walk
/// history, so swiping and back/forward treat them like visited pages.
class InternalPages {
InternalPages(
{required MewsDatabase db,
required LibraryController library,
required Strings strings})
: _db = db,
_library = library,
_strings = strings;
static const historyUrl = 'mews:history';
static const bookmarksUrl = 'mews:bookmarks';
static const subscriptionsUrl = 'mews:subscriptions';
final MewsDatabase _db;
final LibraryController _library;
final Strings _strings;
/// Whether [url] is one of the internal pages.
bool handles(String url) => isInternal(url);
/// Whether [url] belongs to the internal mews: scheme.
static bool isInternal(String url) =>
Uri.tryParse(url)?.scheme == 'mews';
/// The internal page for [url], or null when the name is unknown.
Future<MewsPage?> pageFor(String url) async {
// mews:history is an opaque URI: the page name is the path, not a host.
switch (Uri.tryParse(url)?.path) {
case 'history':
return _history(url);
case 'bookmarks':
return _bookmarks(url);
case 'subscriptions':
return _subscriptions(url);
default:
return null;
}
}
Future<MewsPage> _history(String url) async {
final visits = await _db.recentVisits();
return _page(
url: url,
title: _strings.t('history-title'),
empty: _strings.t('history-empty'),
entries: [
for (final visit in visits)
_Entry(
url: visit.url,
// Dated links are the Mews posts pattern, so history reads like
// any other entry list.
label: '${_day(visit.visitedAt)} ${_clock(visit.visitedAt)}',
title: visit.title ?? visit.url,
),
],
);
}
Future<MewsPage> _bookmarks(String url) async {
final bookmarks = await _db.bookmarksByDate();
return _page(
url: url,
title: _strings.t('bookmarks-title'),
empty: _strings.t('bookmarks-empty'),
entries: [
for (final bookmark in bookmarks)
_Entry(
url: bookmark.url,
label: _day(bookmark.createdAt),
title: bookmark.title ?? bookmark.url,
),
],
);
}
Future<MewsPage> _subscriptions(String url) async {
final subscriptions = await _db.allSubscriptions();
return _page(
url: url,
title: _strings.t('subscriptions-title'),
empty: _strings.t('subscriptions-empty'),
entries: [
for (final subscription in subscriptions)
_Entry(
url: subscription.url,
label: _day(subscription.createdAt),
title: (subscription.title ?? subscription.url) +
_newPostsNote(subscription.url),
),
],
);
}
String _newPostsNote(String siteUrl) {
final count = _library.newEntries.value[siteUrl]?.length ?? 0;
if (count == 0) return '';
final note = count == 1
? _strings.t('subscriptions-new-one')
: _strings.t('subscriptions-new-many', {'count': count});
return ' \u2014 $note';
}
MewsPage _page({
required String url,
required String title,
required String empty,
required List<_Entry> entries,
}) {
final blocks = <MewsBlock>[
HeadingBlock(1, [MewsText(title)]),
if (entries.isEmpty)
ParagraphBlock([MewsText(empty)])
else
ListBlock(
ordered: false,
start: 1,
items: [
for (final entry in entries)
MewsListItem([
ParagraphBlock([
MewsLink(entry.url, [MewsText('${entry.label} ${entry.title}')]),
]),
]),
],
),
];
return MewsPage(
url: url,
// Internal pages carry a marker so they render as Mews pages, not as
// plainly-shown external ones.
specVersion: 'internal',
title: title,
body: blocks,
lang: 'en',
direction: TextDirection.ltr,
variant: MewsVariant.plain,
entries: const [],
);
}
String _day(DateTime date) =>
'${date.year.toString().padLeft(4, '0')}-${_two(date.month)}-${_two(date.day)}';
String _clock(DateTime date) => '${_two(date.hour)}:${_two(date.minute)}';
String _two(int value) => value.toString().padLeft(2, '0');
}
class _Entry {
const _Entry({required this.url, required this.label, required this.title});
final String url;
final String label;
final String title;
}