Ocideck/lib/widgets/dialogs/add_slide_dialog.dart
Brenno de Winter 32ef54e037 Add chart slides (bar/line/pie) with hybrid CSV storage
New "Grafiek" slide type rendering bar, line and pie charts.

Storage fits Marp: a ```chart fenced block holds the spec as JSON. Small
charts keep their data inline (the .md stays self-contained); data-driven
charts link an external CSV via "source": "data/<name>.csv" kept in a
separate data/ directory and packaged into .ocideck like images. On save
the inline data is stripped for linked charts (the CSV is the source of
truth); on open it is re-hydrated from the CSV.

- lib/models/chart.dart: ChartSpec/ChartSeries JSON parse/serialize,
  inline-vs-source handling, and a CSV parser.
- In-app rendering (preview/presenter/PDF/PPTX) via fl_chart.
- HTML export renders charts as self-contained inline SVG generated in
  Dart (no JS chart library); export inlines linked data so the page is
  standalone.
- Editor: type picker, title, a CSV-style data field, and CSV import that
  can inline the data or link it as data/<name>.csv (with unlink).
- Markdown round-trip + .ocideck packaging of linked CSVs; translations
  for all supported languages.

flutter analyze is clean, all tests pass (new chart/CSV/round-trip tests),
and the macOS debug build compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:42:44 +02:00

114 lines
3.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../models/slide.dart';
import '../../theme/app_theme.dart';
import '../../l10n/app_localizations.dart';
class AddSlideDialog extends StatelessWidget {
const AddSlideDialog({super.key});
static Future<SlideType?> show(BuildContext context) {
return showDialog<SlideType>(
context: context,
builder: (_) => const AddSlideDialog(),
);
}
static const _types = [
(SlideType.title, Icons.title, 'Titelpagina'),
(SlideType.section, Icons.bookmark_outline, 'Tussentitel'),
(SlideType.bullets, Icons.format_list_bulleted, 'Alleen Bullets'),
(SlideType.twoBullets, Icons.view_column_outlined, 'Twee Bulletkolommen'),
(
SlideType.bulletsImage,
Icons.view_agenda_outlined,
'Bullets + Afbeelding',
),
(SlideType.twoImages, Icons.auto_stories_outlined, 'Twee Afbeeldingen'),
(SlideType.image, Icons.image_outlined, 'Grote Afbeelding'),
(SlideType.video, Icons.movie_outlined, 'Video'),
(SlideType.quote, Icons.format_quote_outlined, 'Quote'),
(SlideType.table, Icons.table_chart_outlined, 'Tabel'),
(SlideType.chart, Icons.bar_chart, 'Grafiek'),
(SlideType.code, Icons.terminal, 'Broncode'),
(SlideType.freeMarkdown, Icons.code, 'Vrije Markdown'),
];
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.escape): () =>
Navigator.pop(context),
},
child: Focus(
autofocus: true,
child: AlertDialog(
title: Text(l10n.d('Slide type kiezen')),
content: SizedBox(
width: 400,
child: Wrap(
spacing: 10,
runSpacing: 10,
children: _types.map((entry) {
final (type, icon, label) = entry;
return _TypeCard(
icon: icon,
label: l10n.d(label),
onTap: () => Navigator.pop(context, type),
);
}).toList(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.t('cancel')),
),
],
),
),
);
}
}
class _TypeCard extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
const _TypeCard({
required this.icon,
required this.label,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
width: 110,
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFCBD5E1)),
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 28, color: AppTheme.navy),
const SizedBox(height: 8),
Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 11),
),
],
),
),
);
}
}