diff --git a/app/lib/presentation/reader/address_bar.dart b/app/lib/presentation/reader/address_bar.dart index ed817ac..efe1339 100644 --- a/app/lib/presentation/reader/address_bar.dart +++ b/app/lib/presentation/reader/address_bar.dart @@ -1,12 +1,14 @@ +import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform; 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. 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. +/// The single-field address bar. The left icon opens the settings page 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({super.key}); @@ -52,6 +54,14 @@ class _AddressBarState extends State { _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 && @@ -79,12 +89,41 @@ class _AddressBarState extends State { labelText: strings.t('reader-address-label'), isDense: true, border: const OutlineInputBorder(), - prefixIcon: IconButton( - icon: const Icon(Icons.tune), - tooltip: strings.t('reader-open-settings'), - onPressed: () => - deps.browser.open(InternalPages.settingsUrl), - ), + prefixIcon: hasButtons + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.tune), + tooltip: strings.t('reader-open-settings'), + onPressed: () => + deps.browser.open(InternalPages.settingsUrl), + ), + 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, + ), + ], + ) + : IconButton( + icon: const Icon(Icons.tune), + tooltip: strings.t('reader-open-settings'), + onPressed: () => + deps.browser.open(InternalPages.settingsUrl), + ), + prefixIconConstraints: hasButtons + ? const BoxConstraints(minWidth: 138, minHeight: 48) + : null, suffixIcon: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/app/locales/en-US/main.ftl b/app/locales/en-US/main.ftl index 89863e1..50b8949 100644 --- a/app/locales/en-US/main.ftl +++ b/app/locales/en-US/main.ftl @@ -5,6 +5,8 @@ reader-address-placeholder = Enter a web address reader-address-label = Web address reader-reload = Reload this page reader-open-settings = Open settings +reader-back = Go back +reader-forward = Go forward ## Settings page diff --git a/app/test/app_widget_test.dart b/app/test/app_widget_test.dart index ad8c059..34c869f 100644 --- a/app/test/app_widget_test.dart +++ b/app/test/app_widget_test.dart @@ -1,6 +1,7 @@ import 'dart:io' show File; import 'package:actors/actors.dart'; +import 'package:flutter/foundation.dart' show TargetPlatform, debugDefaultTargetPlatformOverride; import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -383,6 +384,45 @@ void main() { expect(find.textContaining('Page A'), findsOneWidget); }); + testWidgets('prev and next buttons navigate on pointer platforms', + (tester) async { + final stubDeps = AppDependencies( + database: MewsDatabase.test(NativeDatabase.memory()), + fetch: _FakeFetch({ + 'https://example.com/a.html': + _page('Page A', '

Page A

The first pages content.

'), + 'https://example.com/b.html': + _page('Page B', '

Page B

The second pages content.

'), + }), + strings: strings, + ); + // Test bindings default to android; a Linux host shows the buttons. + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + + await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings)); + await tester.pumpAndSettle(); + + final back = find.byIcon(Icons.arrow_back); + final forward = find.byIcon(Icons.arrow_forward); + expect(back, findsOneWidget); + expect(forward, findsOneWidget); + + await stubDeps.browser.open('https://example.com/a.html'); + await stubDeps.browser.open('https://example.com/b.html'); + await tester.pumpAndSettle(); + + await tester.tap(back); + await tester.pumpAndSettle(); + expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/a.html'); + + await tester.tap(forward); + await tester.pumpAndSettle(); + expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/b.html'); + + // The invariant check runs before test tear-downs, so reset by hand. + debugDefaultTargetPlatformOverride = null; + }); + testWidgets('reports new posts once per page', (tester) async { await pumpApp(tester);