fix(cards): the browser's actions end at the row's end; phones choose in the grid

From the expanded window on, what can be done to the chosen cards (Move,
Add tags, Delete, …, Done) stood short of the row's end: Select all and
Deselect all were flexible beside a spacer, so Flutter shared the room
between the three and the spacer got a third. Choosing now takes the room
the actions leave, in a row of its own.

On a phone the detail view (a card above a strip) had no way to choose a
card: a tap opened the composer even while choosing. Choosing cards now
shows the grid, where each card shows whether it is chosen, whichever view
was picked, and Done returns to it; the switch between them is off
meanwhile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-11 13:59:33 +02:00
commit e459c972a4
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 84 additions and 37 deletions

View file

@ -331,8 +331,9 @@ class _BrowserState() extends State<_Browser> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_Toolbar(
detailed: detailed,
onDetailed: _setDetailed,
detailed: detailed && !state.selecting,
// Choosing cards is the grid's: the view comes back after.
onDetailed: state.selecting ? null : _setDetailed,
searching: _searchOpen || state.filter.query.isNotEmpty,
onSearch: _toggleSearch,
sort: state.sort,
@ -368,7 +369,9 @@ class _BrowserState() extends State<_Browser> {
Expanded(
child: state.shown.isEmpty
? _NoMatches(onClear: _clearFilters)
: detailed
// Cards are chosen in the grid, where each shows whether
// it is chosen; the detail's tap opens a card.
: detailed && !state.selecting
? _Detail(
cards: state.shown,
current: _current,
@ -498,7 +501,9 @@ typedef _Actions = ({
/// and choosing cards, each tinted while it is at work.
class const _Toolbar({
required final bool detailed,
required final ValueChanged<bool> onDetailed,
/// Switches between grid and detail; off while cards are chosen.
required final ValueChanged<bool>? onDetailed,
required final bool searching,
required final VoidCallback onSearch,
required final CardBrowserSort sort,
@ -541,7 +546,10 @@ class const _Toolbar({
),
],
selected: {detailed},
onSelectionChanged: (chosen) => onDetailed(chosen.single),
onSelectionChanged: switch (onDetailed) {
final change? => (chosen) => change(chosen.single),
null => null,
},
),
const Spacer(),
IconButton(
@ -1289,42 +1297,48 @@ class const _SelectionControls({
children: [
Row(
children: [
// On a narrow phone, in a longer language, they shorten.
Flexible(
child: TextButton(
onPressed: all
? null
: () => context.read<CardBrowserBloc>().add(
const CardBrowserEventAllSelected(),
// Choosing, at the start, takes the room the actions leave,
// so they end at the row's end: beside them, its flexible
// buttons would each claim a share of the room and leave it
// unused.
Expanded(
child: Row(
children: [
// On a narrow phone, in a longer language, they shorten.
Flexible(
child: TextButton(
onPressed: all
? null
: () => context.read<CardBrowserBloc>().add(
const CardBrowserEventAllSelected(),
),
child: Text(
l10n.cardBrowserSelectAll,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
child: Text(
l10n.cardBrowserSelectAll,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
Flexible(
child: TextButton(
onPressed: selected == 0
? null
: () => context.read<CardBrowserBloc>().add(
const CardBrowserEventAllDeselected(),
),
child: Text(
l10n.cardBrowserDeselectAll,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
const SizedBox(width: 8),
_CountPill(count: selected),
],
),
),
Flexible(
child: TextButton(
onPressed: selected == 0
? null
: () => context.read<CardBrowserBloc>().add(
const CardBrowserEventAllDeselected(),
),
child: Text(
l10n.cardBrowserDeselectAll,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
if (actions == null) ...[
const SizedBox(width: 8),
_CountPill(count: selected),
] else ...[
const SizedBox(width: 8),
_CountPill(count: selected),
const Spacer(),
if (actions != null) ...[
OutlinedButton.icon(
onPressed: actions.move,
icon: const Icon(LucideIcons.folderInput, size: 18),

View file

@ -3,6 +3,7 @@ import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/card_composer_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import 'package:material_ui/material_ui.dart';
@ -277,4 +278,36 @@ void main() {
expect(find.byType(PageView), findsOneWidget);
expect(find.byTooltip('Next'), findsNothing);
});
testWidgets('from the expanded window on, what can be done to the chosen '
"cards ends at the row's end", (tester) async {
await show(tester);
await tester.tap(find.byTooltip('Select'));
await tester.pumpAndSettle();
final done = tester.getRect(find.widgetWithText(FilledButton, 'Done'));
final row = tester.getRect(find.byTooltip('Select').first);
expect(done.right, moreOrLessEquals(row.right, epsilon: 4));
});
testWidgets('on a phone in the detail view, choosing cards shows the '
'grid, where a tap chooses a card rather than opening it; Done '
'returns to the detail', (tester) async {
await show(tester, width: 400);
await tester.tap(find.byTooltip('Detail'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Select'));
await tester.pumpAndSettle();
expect(find.byType(PageView), findsNothing);
await tester.tap(find.text('cat'));
await tester.pumpAndSettle();
expect(find.text('1 selected'), findsOneWidget);
expect(find.byType(CardComposerScreen), findsNothing);
await tester.tap(find.text('Done'));
await tester.pumpAndSettle();
expect(find.byType(PageView), findsOneWidget);
});
}