Ocideck/lib/widgets/slides/previews/table_preview.dart
Brenno de Winter b719c43991 Add presentation timer / rehearsal mode to the presenter
The presenter view now doubles as a rehearsal clock that measures without
coaching: a countdown against a target time, the time spent on the current
slide, and an end-of-run summary (total vs. target and per-slide times, with
copy-to-clipboard). Timing lives in a plain, unit-tested RehearsalController fed
via an idempotent observe() on every build, so it captures every navigation
path. The default target is stored in AppSettings; live adjustment is the K key
(typed as MMSS). All rehearsal state is session-only -- nothing is written to
disk or into the .md file.

- New: models/rehearsal.dart, services/rehearsal_controller.dart,
  widgets/presentation/rehearsal_summary.dart, plus a controller unit test.
- Presenter: countdown + per-slide timer in the clock bar, K to set the target,
  R resets the run, end-of-run summary dialog, and help/cheatsheet entries.
- Settings: presentationTargetSeconds (default target) with a dropdown in the
  General tab, threaded into FullscreenPresenter.present().
- l10n: new Dutch source strings translated in all seven languages.
- Docs: README, CHANGELOG, USER_GUIDE, SHORTCUTS, ARCHITECTURE.

Also bundles a pre-existing in-progress change already in the working tree: wire
the existing ThemeProfile.tableHeaderBackgroundColor into table rendering
(preview, HTML export, file_service) and the settings dialog, plus its
translations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 07:03:08 +02:00

130 lines
4.1 KiB
Dart

// Part of the slide_preview library — see ../slide_preview.dart.
// Split out for navigability; all imports live in the main library file.
part of '../slide_preview.dart';
class _TablePreview extends StatelessWidget {
final Slide slide;
final double w;
final String font;
final ThemeProfile profile;
const _TablePreview({
required this.slide,
required this.w,
required this.font,
required this.profile,
});
@override
Widget build(BuildContext context) {
final pad = w * 0.06;
final safe = slide.showLogo
? _splitTextLogoSafeInsets(w, profile)
: EdgeInsets.zero;
final titleSize = w * 0.038;
final rows = slide.tableRows.where((r) => r.isNotEmpty).toList();
final colCount = rows.fold<int>(0, (m, r) => r.length > m ? r.length : m);
// Scale cell text down as the table grows so it keeps fitting nicely.
final density = (rows.length + colCount).clamp(2, 24);
final cellSize = (w * 0.025 * (10 / (density + 6))).clamp(
w * 0.010,
w * 0.021,
);
final accent = _hexColor(profile.accentColor);
final textColor = _hexColor(profile.tableTextColor);
final headerTextColor = _hexColor(profile.tableHeaderTextColor);
final headerBackground = _hexColor(profile.tableHeaderBackgroundColor);
final borderColor = accent.withValues(alpha: 0.35);
Widget cell(String value, {required bool header}) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: cellSize * 0.55,
vertical: cellSize * 0.36,
),
child: _md(
context,
value,
_applyFont(
font,
TextStyle(
fontSize: cellSize,
color: header ? headerTextColor : textColor,
fontWeight: header ? FontWeight.bold : FontWeight.normal,
),
),
linkColor: header ? headerTextColor : accent,
),
);
}
TableRow buildRow(List<String> row, {required bool header}) {
return TableRow(
decoration: BoxDecoration(color: header ? headerBackground : null),
children: List.generate(colCount, (c) {
final value = c < row.length ? row[c] : '';
return TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: cell(value, header: header),
);
}),
);
}
return Container(
color: _hexColor(profile.slideBackgroundColor),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.topLeft,
child: SizedBox(
width: w,
child: Padding(
padding: EdgeInsets.fromLTRB(
pad,
pad + safe.top,
pad,
pad + safe.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (slide.title.isNotEmpty) ...[
_md(
context,
slide.title,
_applyFont(
font,
TextStyle(
fontSize: titleSize,
fontWeight: FontWeight.bold,
color: _hexColor(profile.textColor),
),
),
linkColor: _hexColor(profile.accentColor),
),
SizedBox(height: pad * 0.35),
],
if (rows.isNotEmpty && colCount > 0)
Table(
border: TableBorder.all(
color: borderColor,
width: w * 0.0012,
),
defaultColumnWidth: const FlexColumnWidth(),
children: [
buildRow(rows.first, header: true),
for (var i = 1; i < rows.length; i++)
buildRow(rows[i], header: false),
],
),
],
),
),
),
),
);
}
}