feat: retire FOB and sheet; settings as a Mews page and actions on the address bar
This commit is contained in:
parent
6f9c0e587a
commit
28ff7ca327
10 changed files with 324 additions and 361 deletions
|
|
@ -24,7 +24,8 @@ class AppDependencies {
|
|||
strings = strings ?? Strings() {
|
||||
settings = ReaderSettingsController(db);
|
||||
library = LibraryController(db);
|
||||
internalPages = InternalPages(db: db, library: library, strings: this.strings);
|
||||
internalPages = InternalPages(
|
||||
db: db, library: library, settings: settings, strings: this.strings);
|
||||
browser = BrowserController(db: db, fetch: this.fetch, internalPages: internalPages);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue