the-mews/app/lib/presentation/reader/address_bar.dart

184 lines
6.7 KiB
Dart
Raw Normal View History

import 'package:flutter/foundation.dart'
show defaultTargetPlatform, TargetPlatform;
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart' show SignalBuilder;
import '../../application/pages/mews_urls.dart';
import '../app.dart';
/// The single-field address bar. The left icon opens the hub and, on pointer
/// platforms, carries the back and forward buttons; 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({this.focusNode, super.key});
/// The reader can ask for the field's focus from outside, for a keyboard
/// shortcut. When null the bar owns its own node.
final FocusNode? focusNode;
@override
State<AddressBar> createState() => _AddressBarState();
}
class _AddressBarState extends State<AddressBar> {
final _controller = TextEditingController();
late final FocusNode _focus = widget.focusNode ?? FocusNode();
bool _focused = false;
@override
void initState() {
super.initState();
2026-10-12 17:59:14 +03:00
_focus.addListener(_onFocusChange);
}
void _onFocusChange() {
if (_focus.hasFocus != _focused) {
setState(() => _focused = _focus.hasFocus);
if (_focused) {
_controller.selection = TextSelection(
baseOffset: 0,
extentOffset: _controller.text.length,
);
}
2026-10-12 17:59:14 +03:00
}
}
@override
void dispose() {
2026-10-12 17:59:14 +03:00
_focus.removeListener(_onFocusChange);
// A focus node handed in from outside belongs to whoever handed it in.
if (widget.focusNode == null) _focus.dispose();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final deps = AppScope.of(context).deps;
final strings = AppScope.of(context).strings;
return SignalBuilder(
builder: (context) {
final page = deps.browser.currentPage.value;
final url = page?.url;
if (url != null && !_focused) {
_controller.text = url;
}
// Touch devices navigate with swipes; desktops get buttons.
final hasButtons = switch (defaultTargetPlatform) {
TargetPlatform.linux ||
TargetPlatform.windows ||
TargetPlatform.macOS => true,
_ => false,
};
// Only pages with dated entry links or an Atom feed can be subscribed
// to (mews spec, section 6).
final canSubscribe =
page != null &&
!MewsUrls.isInternal(page.url) &&
(page.atomFeedHref != null || page.entries.isNotEmpty);
final canBookmark = page != null && !MewsUrls.isInternal(page.url);
// One button, two layouts: on pointer platforms it sits beside the
// back and forward buttons.
final hubButton = IconButton(
icon: const Icon(Icons.menu),
tooltip: strings.t('reader-open-hub'),
onPressed: () => deps.browser.open(MewsUrls.hub),
);
return Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
// The label is for screen readers only: the placeholder is what a
// sighted reader sees, and a visible labelText would repeat it.
child: Semantics(
label: strings.t('reader-address-label'),
textField: true,
child: TextField(
controller: _controller,
focusNode: _focus,
autofocus: false,
keyboardType: TextInputType.url,
textInputAction: TextInputAction.go,
autocorrect: false,
enableSuggestions: false,
onSubmitted: (value) {
_focus.unfocus();
deps.browser.open(value);
},
decoration: InputDecoration(
hintText: strings.t('reader-address-placeholder'),
isDense: true,
border: const OutlineInputBorder(),
prefixIcon: hasButtons
? Row(
mainAxisSize: MainAxisSize.min,
children: [
hubButton,
IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: strings.t('reader-back'),
onPressed: deps.browser.canGoBack
? deps.browser.back
: null,
),
IconButton(
icon: const Icon(Icons.arrow_forward),
tooltip: strings.t('reader-forward'),
onPressed: deps.browser.canGoForward
? deps.browser.forward
: null,
),
],
)
: hubButton,
prefixIconConstraints: hasButtons
? const BoxConstraints(minWidth: 138, minHeight: 48)
: null,
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.subscriptions.isSubscribed(page.url)
? Icons.notifications_active
: Icons.notifications_outlined,
),
tooltip: deps.subscriptions.isSubscribed(page.url)
? strings.t('reader-unsubscribe')
: strings.t('reader-subscribe'),
onPressed: () => deps.subscriptions.toggle(page),
),
if (canBookmark)
IconButton(
icon: Icon(
deps.bookmarks.isBookmarked(page.url)
? Icons.star
: Icons.star_border,
),
tooltip: deps.bookmarks.isBookmarked(page.url)
? strings.t('reader-bookmark-remove')
: strings.t('reader-bookmark-add'),
onPressed: () =>
deps.bookmarks.toggle(page.url, page.title),
),
],
),
),
),
),
);
},
);
}
}