2026-10-11 22:07:25 +03:00
|
|
|
import 'package:flutter/foundation.dart'
|
|
|
|
|
show defaultTargetPlatform, TargetPlatform;
|
2026-10-11 09:33:25 +03:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:signals/signals_flutter.dart' show SignalBuilder;
|
|
|
|
|
|
2026-10-11 13:05:07 +03:00
|
|
|
import '../../application/pages/mews_urls.dart';
|
2026-10-11 09:33:25 +03:00
|
|
|
import '../app.dart';
|
|
|
|
|
|
2026-10-11 13:05:07 +03:00
|
|
|
/// 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.
|
2026-10-11 09:33:25 +03:00
|
|
|
class AddressBar extends StatefulWidget {
|
2026-10-11 22:32:44 +03:00
|
|
|
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;
|
2026-10-11 09:33:25 +03:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<AddressBar> createState() => _AddressBarState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _AddressBarState extends State<AddressBar> {
|
|
|
|
|
final _controller = TextEditingController();
|
2026-10-11 22:32:44 +03:00
|
|
|
late final FocusNode _focus = widget.focusNode ?? FocusNode();
|
2026-10-11 09:33:25 +03:00
|
|
|
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-11 09:33:25 +03:00
|
|
|
}
|
2026-10-12 17:59:14 +03:00
|
|
|
}
|
2026-10-11 09:33:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
2026-10-12 17:59:14 +03:00
|
|
|
_focus.removeListener(_onFocusChange);
|
2026-10-11 22:32:44 +03:00
|
|
|
// A focus node handed in from outside belongs to whoever handed it in.
|
|
|
|
|
if (widget.focusNode == null) _focus.dispose();
|
2026-10-11 09:33:25 +03:00
|
|
|
_controller.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final deps = AppScope.of(context).deps;
|
|
|
|
|
final strings = AppScope.of(context).strings;
|
|
|
|
|
|
2026-10-11 22:07:25 +03:00
|
|
|
return SignalBuilder(
|
|
|
|
|
builder: (context) {
|
|
|
|
|
final page = deps.browser.currentPage.value;
|
|
|
|
|
final url = page?.url;
|
|
|
|
|
if (url != null && !_focused) {
|
|
|
|
|
_controller.text = url;
|
|
|
|
|
}
|
2026-10-11 12:14:32 +03:00
|
|
|
|
2026-10-11 22:07:25 +03:00
|
|
|
// Touch devices navigate with swipes; desktops get buttons.
|
|
|
|
|
final hasButtons = switch (defaultTargetPlatform) {
|
|
|
|
|
TargetPlatform.linux ||
|
|
|
|
|
TargetPlatform.windows ||
|
|
|
|
|
TargetPlatform.macOS => true,
|
|
|
|
|
_ => false,
|
|
|
|
|
};
|
2026-10-11 12:39:44 +03:00
|
|
|
|
2026-10-11 22:07:25 +03:00
|
|
|
// 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);
|
2026-10-11 13:05:07 +03:00
|
|
|
|
2026-10-11 22:07:25 +03:00
|
|
|
// 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),
|
|
|
|
|
);
|
2026-10-11 12:14:32 +03:00
|
|
|
|
2026-10-11 22:07:25 +03:00
|
|
|
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)
|
2026-10-11 12:39:44 +03:00
|
|
|
IconButton(
|
2026-10-11 22:07:25 +03:00
|
|
|
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),
|
2026-10-11 12:39:44 +03:00
|
|
|
),
|
2026-10-11 22:07:25 +03:00
|
|
|
if (canBookmark)
|
2026-10-11 12:39:44 +03:00
|
|
|
IconButton(
|
2026-10-11 22:07:25 +03:00
|
|
|
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),
|
2026-10-11 12:39:44 +03:00
|
|
|
),
|
2026-10-11 22:07:25 +03:00
|
|
|
],
|
2026-10-11 12:14:32 +03:00
|
|
|
),
|
2026-10-11 22:07:25 +03:00
|
|
|
),
|
2026-10-11 12:14:32 +03:00
|
|
|
),
|
2026-10-11 09:33:25 +03:00
|
|
|
),
|
2026-10-11 22:07:25 +03:00
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-10-11 09:33:25 +03:00
|
|
|
}
|
|
|
|
|
}
|