Small UX tweaks: tab close affordance and date quick-fill
Some checks failed
CI / Format · Analyze · Test (push) Has been cancelled
CI / Format · Analyze · Test (pull_request) Has been cancelled

- Show the tab close button for an open presentation tab even when it is the
  only tab.
- Double-click the date field in the presentation-info dialog to fill in
  today's date.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Brenno de Winter 2026-06-08 21:50:23 +02:00
parent 9827715873
commit ebc9710283
4 changed files with 83 additions and 3 deletions

View file

@ -550,7 +550,8 @@ class _AppTabBar extends StatelessWidget {
_TabChip(
tab: tabsState.tabs[i],
isActive: i == tabsState.clampedIndex,
showClose: tabsState.tabs.length > 1,
showClose:
tabsState.tabs.length > 1 || tabsState.tabs[i].isOpen,
panelText: palette.panelText,
accent: Theme.of(context).colorScheme.secondary,
onTap: () => onSelect(i),

View file

@ -91,6 +91,13 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
);
}
void _setCurrentDate() {
final now = DateTime.now();
String twoDigits(int value) => value.toString().padLeft(2, '0');
_date.text = '${now.year}-${twoDigits(now.month)}-${twoDigits(now.day)}';
_date.selection = TextSelection.collapsed(offset: _date.text.length);
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
@ -143,7 +150,12 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
const SizedBox(width: 12),
SizedBox(
width: 120,
child: _field(_date, 'Datum', 'Bijv. 2026-05-30'),
child: _field(
_date,
'Datum',
'Bijv. 2026-05-30',
onDoubleTap: _setCurrentDate,
),
),
],
),
@ -190,9 +202,10 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
String label,
String hint, {
int maxLines = 1,
VoidCallback? onDoubleTap,
}) {
final l10n = context.l10n;
return TextField(
final field = TextField(
controller: controller,
maxLines: maxLines,
decoration: InputDecoration(
@ -202,5 +215,11 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
border: const OutlineInputBorder(),
),
);
if (onDoubleTap == null) return field;
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTap: onDoubleTap,
child: field,
);
}
}

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ocideck/models/deck.dart';
import 'package:ocideck/widgets/dialogs/presentation_info_dialog.dart';
void main() {
testWidgets('double-clicking date fills in the current date', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: PresentationInfoDialog(deck: Deck(title: 'Test')),
),
),
);
final dateField = find.byWidgetPredicate(
(widget) =>
widget is TextField && widget.decoration?.labelText == 'Datum',
);
final now = DateTime.now();
String twoDigits(int value) => value.toString().padLeft(2, '0');
final expected =
'${now.year}-${twoDigits(now.month)}-${twoDigits(now.day)}';
await tester.tap(dateField);
await tester.pump(const Duration(milliseconds: 50));
await tester.tap(dateField);
await tester.pump(const Duration(milliseconds: 100));
final field = tester.widget<TextField>(dateField);
expect(field.controller!.text, expected);
});
}

View file

@ -2,6 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ocideck/app.dart';
import 'package:ocideck/models/deck.dart';
import 'package:ocideck/models/slide.dart';
import 'package:ocideck/state/tabs_provider.dart';
import 'package:ocideck/widgets/app_shell.dart';
void main() {
testWidgets('Welcome screen shows startup logo', (WidgetTester tester) async {
@ -16,4 +20,27 @@ void main() {
await tester.pumpWidget(const ProviderScope(child: OciDeckApp()));
expect(find.byIcon(Icons.settings_outlined), findsOneWidget);
});
testWidgets('the only open presentation can be closed', (tester) async {
await tester.binding.setSurfaceSize(const Size(1600, 1000));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(const ProviderScope(child: OciDeckApp()));
final container = ProviderScope.containerOf(
tester.element(find.byType(AppShell)),
);
final tab = container.read(tabsProvider).current!;
tab.deckNotifier.loadDeck(
Deck(
title: 'Test',
slides: [Slide.create(SlideType.title).copyWith(title: 'Test')],
),
);
await tester.pump();
expect(find.byIcon(Icons.close), findsOneWidget);
await tester.tap(find.byIcon(Icons.close));
await tester.pump();
expect(container.read(tabsProvider).current!.isOpen, isFalse);
});
}