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( _TabChip(
tab: tabsState.tabs[i], tab: tabsState.tabs[i],
isActive: i == tabsState.clampedIndex, isActive: i == tabsState.clampedIndex,
showClose: tabsState.tabs.length > 1, showClose:
tabsState.tabs.length > 1 || tabsState.tabs[i].isOpen,
panelText: palette.panelText, panelText: palette.panelText,
accent: Theme.of(context).colorScheme.secondary, accent: Theme.of(context).colorScheme.secondary,
onTap: () => onSelect(i), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
@ -143,7 +150,12 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
const SizedBox(width: 12), const SizedBox(width: 12),
SizedBox( SizedBox(
width: 120, 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 label,
String hint, { String hint, {
int maxLines = 1, int maxLines = 1,
VoidCallback? onDoubleTap,
}) { }) {
final l10n = context.l10n; final l10n = context.l10n;
return TextField( final field = TextField(
controller: controller, controller: controller,
maxLines: maxLines, maxLines: maxLines,
decoration: InputDecoration( decoration: InputDecoration(
@ -202,5 +215,11 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
border: const OutlineInputBorder(), 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_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:ocideck/app.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() { void main() {
testWidgets('Welcome screen shows startup logo', (WidgetTester tester) async { testWidgets('Welcome screen shows startup logo', (WidgetTester tester) async {
@ -16,4 +20,27 @@ void main() {
await tester.pumpWidget(const ProviderScope(child: OciDeckApp())); await tester.pumpWidget(const ProviderScope(child: OciDeckApp()));
expect(find.byIcon(Icons.settings_outlined), findsOneWidget); 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);
});
} }