From ebc9710283c305e66d5a89bd16b71cf74a1ca365 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Mon, 8 Jun 2026 21:50:23 +0200 Subject: [PATCH] Small UX tweaks: tab close affordance and date quick-fill - 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 --- lib/widgets/app_shell.dart | 3 +- .../dialogs/presentation_info_dialog.dart | 23 +++++++++++-- test/presentation_info_dialog_test.dart | 33 +++++++++++++++++++ test/widget_test.dart | 27 +++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 test/presentation_info_dialog_test.dart diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index c9611b3..48ffbc2 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -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), diff --git a/lib/widgets/dialogs/presentation_info_dialog.dart b/lib/widgets/dialogs/presentation_info_dialog.dart index 2535395..431e136 100644 --- a/lib/widgets/dialogs/presentation_info_dialog.dart +++ b/lib/widgets/dialogs/presentation_info_dialog.dart @@ -91,6 +91,13 @@ class _PresentationInfoDialogState extends State { ); } + 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 { 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 { 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 { border: const OutlineInputBorder(), ), ); + if (onDoubleTap == null) return field; + return GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTap: onDoubleTap, + child: field, + ); } } diff --git a/test/presentation_info_dialog_test.dart b/test/presentation_info_dialog_test.dart new file mode 100644 index 0000000..0b78317 --- /dev/null +++ b/test/presentation_info_dialog_test.dart @@ -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(dateField); + expect(field.controller!.text, expected); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart index 52890ac..de17730 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -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); + }); }