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>
130 lines
4.1 KiB
Dart
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),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|