reader pin UX: push-pin icon, toggle, left/right split

Per user feedback on the reader action bar: use the push-pin glyph (not
the chain/link), make the pin button toggle the pinned state (tap to pin
current, tap again to unpin) via ReaderNav.togglePin, and split the
layout so the pin/unpin toggle sits on the left while jump-to-pin joins
the navigator (back/forward) on the right — left toggles, right
navigates. The action button gains an active (accent) state for the
pinned indicator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 13:31:51 +02:00
co-authored by Claude Opus 4.8
parent 0eb7b0df2f
commit f83e52b818
11 changed files with 108 additions and 156 deletions
@@ -725,7 +725,7 @@ void main() {
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Pin current',
(w) => w is Semantics && w.properties.label == 'Pin',
)
.first);
await pumpAsync(tester);
@@ -742,7 +742,7 @@ void main() {
// Pin D-1.
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Pin current',
(w) => w is Semantics && w.properties.label == 'Pin',
)
.first);
await pumpAsync(tester);
@@ -763,41 +763,32 @@ void main() {
expect(find.text('Decision D-1'), findsWidgets);
});
testWidgets('pin replaces previous pin', (tester) async {
testWidgets('pin toggles off on a second tap (unpin)', (tester) async {
await pumpView(tester, initialId: 'D-1');
// Pin D-1.
// Pin D-1 → the jump-to-pin button appears in the navigator.
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Pin current',
(w) => w is Semantics && w.properties.label == 'Pin',
)
.first);
await pumpAsync(tester);
expect(
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'),
findsOneWidget,
);
// Navigate to D-2.
await open(tester, 'D-2');
// Replace pin with D-2.
// Tapping the toggle again (now 'Unpin') clears the pin.
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Replace pin',
(w) => w is Semantics && w.properties.label == 'Unpin',
)
.first);
await pumpAsync(tester);
// Navigate to D-3.
await open(tester, 'D-3');
// Jump to pin — should go to D-2 (replaced), not D-1.
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Jump to pin',
)
.first);
await pumpAsync(tester);
// Title appears in pane header subtitle + body card.
expect(find.text('Decision D-2'), findsWidgets);
expect(
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'),
findsNothing,
);
});
});
+17 -25
View File
@@ -326,7 +326,7 @@ void main() {
await loadFile(tester, f, 'a.md');
expect(
find.byWidgetPredicate((w) => w is Semantics && (w.properties.label == 'Pin current' || w.properties.label == 'Replace pin')),
find.byWidgetPredicate((w) => w is Semantics && (w.properties.label == 'Pin' || w.properties.label == 'Unpin')),
findsOneWidget,
);
});
@@ -350,7 +350,7 @@ void main() {
// Tap Pin current.
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Pin current',
(w) => w is Semantics && w.properties.label == 'Pin',
)
.first);
await pumpAsync(tester);
@@ -370,7 +370,7 @@ void main() {
// Pin a.md.
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Pin current',
(w) => w is Semantics && w.properties.label == 'Pin',
)
.first);
await pumpAsync(tester);
@@ -391,42 +391,34 @@ void main() {
expect(find.text('a.md'), findsOneWidget);
});
testWidgets('pin replaces previous pin', (tester) async {
_stubReadMap(f, {'a.md': 'A', 'b.md': 'B', 'c.md': 'C'});
testWidgets('pin toggles off on a second tap (unpin)', (tester) async {
_stubReadMap(f, {'a.md': 'A', 'b.md': 'B'});
await pumpView(tester, f);
await loadFile(tester, f, 'a.md');
// Pin a.md.
// Pin a.md → jump-to-pin appears.
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Pin current',
(w) => w is Semantics && w.properties.label == 'Pin',
)
.first);
await pumpAsync(tester);
expect(
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'),
findsOneWidget,
);
// Navigate to b.md.
await loadFile(tester, f, 'b.md');
// Replace pin with b.md.
// Tap the toggle again (now 'Unpin') → pin cleared.
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Replace pin',
(w) => w is Semantics && w.properties.label == 'Unpin',
)
.first);
await pumpAsync(tester);
// Navigate to c.md.
await loadFile(tester, f, 'c.md');
// Jump to pin should go to b.md now (replaced), not a.md.
await tester.tap(find
.byWidgetPredicate(
(w) => w is Semantics && w.properties.label == 'Jump to pin',
)
.first);
await pumpAsync(tester);
expect(find.text('b.md'), findsOneWidget);
expect(
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'),
findsNothing,
);
});
});
+9
View File
@@ -115,6 +115,15 @@ void main() {
expect(loads.last, 'D-1');
});
test('togglePin pins the current entry, then clears it', () {
nav.open('D-1');
expect(nav.hasPinned, isFalse);
nav.togglePin();
expect(nav.hasPinned, isTrue);
nav.togglePin();
expect(nav.hasPinned, isFalse);
});
test('pin replaces the previous pin', () {
nav.open('D-1');
nav.pin();