feat: retire FOB and sheet; settings as a Mews page and actions on the address bar

This commit is contained in:
randogoth 2026-10-11 12:14:32 +03:00
parent 6f9c0e587a
commit 28ff7ca327
10 changed files with 324 additions and 361 deletions

View file

@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart' show SignalBuilder;
import '../../application/internal_pages.dart';
import '../app.dart';
/// The single-field address bar at the top of the reader. Submitting navigates;
/// the field shows the current page's URL whenever the reader is not typing.
/// The single-field address bar. The left icon opens the settings page; the
/// right icons reload, subscribe (only when the page offers something to
/// subscribe to) and bookmark the current page.
class AddressBar extends StatefulWidget {
const AddressBar({super.key});
@ -44,10 +46,20 @@ class _AddressBarState extends State<AddressBar> {
final strings = AppScope.of(context).strings;
return SignalBuilder(builder: (context) {
final url = deps.browser.currentPage.value?.url;
final page = deps.browser.currentPage.value;
final url = page?.url;
if (url != null && !_focused) {
_controller.text = url;
}
// Only pages with dated entry links or an Atom feed can be subscribed
// to (mews spec, section 6).
final canSubscribe = page != null &&
!InternalPages.isInternal(page.url) &&
(page.atomFeedHref != null || page.entries.isNotEmpty);
final canBookmark =
page != null && !InternalPages.isInternal(page.url);
return Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
child: TextField(
@ -65,13 +77,50 @@ class _AddressBarState extends State<AddressBar> {
decoration: InputDecoration(
hintText: strings.t('reader-address-placeholder'),
labelText: strings.t('reader-address-label'),
prefixIcon: IconButton(
icon: const Icon(Icons.refresh),
tooltip: strings.t('reader-reload'),
onPressed: () => deps.browser.reload(),
),
isDense: true,
border: const OutlineInputBorder(),
prefixIcon: IconButton(
icon: const Icon(Icons.tune),
tooltip: strings.t('reader-open-settings'),
onPressed: () =>
deps.browser.open(InternalPages.settingsUrl),
),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: strings.t('reader-reload'),
onPressed: () => deps.browser.reload(),
),
if (canSubscribe)
IconButton(
icon: Icon(
deps.library.isSubscribed(page.url)
? Icons.notifications_active
: Icons.notifications_outlined,
),
tooltip: deps.library.isSubscribed(page.url)
? strings.t('settings-unsubscribe')
: strings.t('settings-subscribe'),
onPressed: () => deps.library
.toggleSubscription(page.url, page.title, page.entries),
),
if (canBookmark)
IconButton(
icon: Icon(
deps.library.isBookmarked(page.url)
? Icons.star
: Icons.star_border,
),
tooltip: deps.library.isBookmarked(page.url)
? strings.t('settings-bookmark-remove')
: strings.t('settings-bookmark-add'),
onPressed: () =>
deps.library.toggleBookmark(page.url, page.title),
),
],
),
),
),
);

View file

@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
/// The floating control that appears when the reader scrolls, opening the
/// reader settings bottom sheet.
class Fob extends StatelessWidget {
const Fob({required this.visible, required this.label, required this.onTap, super.key});
final bool visible;
final String label;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: visible ? 1 : 0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: IgnorePointer(
ignoring: !visible,
child: Padding(
padding: const EdgeInsets.all(16),
child: Semantics(
label: label,
button: true,
child: FloatingActionButton(
onPressed: onTap,
child: const Icon(Icons.tune),
),
),
),
),
);
}
}

View file

@ -8,10 +8,8 @@ import '../../mews/document.dart';
import '../../mews/parser.dart' show mewsPlainText;
import '../app.dart';
import 'address_bar.dart';
import 'fob.dart';
import 'mews_page_view.dart';
import 'palettes.dart';
import 'settings_sheet.dart';
import 'swipe_navigator.dart';
/// The whole v1 UI: an address bar that hides on scroll, the page reader with
@ -27,8 +25,8 @@ class ReaderScreen extends StatefulWidget {
class _ReaderScreenState extends State<ReaderScreen> {
final _scroll = ScrollController();
/// Address bar and FOB share one visibility: on screen from the start,
/// hidden while the reader scrolls down, back on any upward scroll.
/// The address bar is on screen from the start, hidden while the reader
/// scrolls down, back on any upward scroll.
bool _chromeVisible = true;
bool _showBrokenText = false;
final _reportedNewPosts = <String>{};
@ -63,9 +61,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
@override
Widget build(BuildContext context) {
final scope = AppScope.of(context);
final deps = scope.deps;
final strings = scope.strings;
final deps = AppScope.of(context).deps;
return Scaffold(
body: Column(
@ -116,15 +112,6 @@ class _ReaderScreenState extends State<ReaderScreen> {
child: Stack(
children: [
Positioned.fill(child: _body(context, page)),
Positioned(
right: 0,
bottom: 0,
child: Fob(
visible: _chromeVisible,
label: strings.t('reader-fob-label'),
onTap: () => showReaderSettingsSheet(context),
),
),
if (browser.loading.value)
const Positioned(
top: 8,

View file

@ -1,193 +0,0 @@
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';
/// Opens the reader settings bottom sheet: fonts, text size, theme, colors,
/// bookmark and subscription for the current page.
void showReaderSettingsSheet(BuildContext context) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (context) => const SettingsSheet(),
);
}
class SettingsSheet extends StatelessWidget {
const SettingsSheet({super.key});
@override
Widget build(BuildContext context) {
final scope = AppScope.of(context);
final deps = scope.deps;
final strings = scope.strings;
return SignalBuilder(builder: (context) {
final settings = deps.settings;
final library = deps.library;
final page = deps.browser.currentPage.value;
final url = page?.url;
final percent = (settings.fontScale.value * 100).round();
return SafeArea(
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(0, 12, 0, 16),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
strings.t('settings-sheet-title'),
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,
onChanged: (value) => settings.setFont(value!),
child: Column(
children: [
for (final font in readerFonts)
RadioListTile<String>(
value: font.family,
title: Text(strings.t(font.labelId)),
),
],
),
),
_heading(context, strings.t('settings-size-heading')),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
tooltip: strings.t('settings-size-decrease'),
onPressed: settings.fontScale.value > ReaderSettingsController.minScale
? settings.decreaseSize
: null,
icon: const Icon(Icons.remove),
),
SizedBox(
width: 56,
child: Text(
'$percent%',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
),
),
IconButton(
tooltip: strings.t('settings-size-increase'),
onPressed: settings.fontScale.value < ReaderSettingsController.maxScale
? settings.increaseSize
: null,
icon: const Icon(Icons.add),
),
],
),
_heading(context, strings.t('settings-theme-heading')),
RadioGroup<String>(
groupValue: settings.themeMode.value,
onChanged: (value) => settings.setThemeMode(value!),
child: Column(
children: [
for (final (mode, id) in [
('light', 'settings-theme-light'),
('dark', 'settings-theme-dark'),
('system', 'settings-theme-system'),
])
RadioListTile<String>(
value: mode,
title: Text(strings.t(id)),
),
],
),
),
_heading(context, strings.t('settings-variant-heading')),
RadioGroup<String>(
groupValue: settings.variant.value,
onChanged: (value) => settings.setVariant(value!),
child: Column(
children: [
for (final (variant, id) in [
('site', 'settings-variant-site'),
('warm', 'settings-variant-warm'),
('cool', 'settings-variant-cool'),
('green', 'settings-variant-green'),
('mono', 'settings-variant-mono'),
])
RadioListTile<String>(
value: variant,
title: Text(strings.t(id)),
),
],
),
),
const Divider(),
if (url != null && !InternalPages.isInternal(url)) ...[
ListTile(
leading: Icon(library.isBookmarked(url) ? Icons.star : Icons.star_border),
title: Text(
library.isBookmarked(url)
? strings.t('settings-bookmark-remove')
: strings.t('settings-bookmark-add'),
),
onTap: () => library.toggleBookmark(url, page?.title),
),
ListTile(
leading: Icon(library.isSubscribed(url)
? Icons.notifications_active
: Icons.notifications_outlined),
title: Text(
library.isSubscribed(url)
? strings.t('settings-unsubscribe')
: strings.t('settings-subscribe'),
),
subtitle: library.isSubscribed(url)
? Text(strings.t('settings-subscribed-notice'))
: null,
onTap: () => library.toggleSubscription(url, page?.title, page?.entries ?? const []),
),
],
],
),
);
});
}
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(
text,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(color: Theme.of(context).colorScheme.primary),
),
);
}