2026-06-06 20:41:24 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import '../../models/slide.dart';
|
|
|
|
|
import '../../l10n/app_localizations.dart';
|
|
|
|
|
import '_editor_field.dart';
|
|
|
|
|
|
|
|
|
|
/// Editor voor een broncode-slide: een optionele titel, een keuzelijst voor de
|
|
|
|
|
/// programmeertaal (voor syntaxkleuring) en een monospace tekstveld voor de code.
|
|
|
|
|
class CodeEditor extends StatefulWidget {
|
|
|
|
|
final Slide slide;
|
|
|
|
|
final ValueChanged<Slide> onUpdate;
|
|
|
|
|
|
|
|
|
|
const CodeEditor({super.key, required this.slide, required this.onUpdate});
|
|
|
|
|
|
|
|
|
|
/// Veelgebruikte talen. De waarde is de highlight.js-id; een lege waarde
|
|
|
|
|
/// betekent platte tekst (geen kleuring).
|
|
|
|
|
static const _languages = <(String, String)>[
|
|
|
|
|
('', 'Platte tekst'),
|
|
|
|
|
('dart', 'Dart'),
|
|
|
|
|
('javascript', 'JavaScript'),
|
|
|
|
|
('typescript', 'TypeScript'),
|
|
|
|
|
('python', 'Python'),
|
|
|
|
|
('java', 'Java'),
|
|
|
|
|
('kotlin', 'Kotlin'),
|
|
|
|
|
('swift', 'Swift'),
|
|
|
|
|
('csharp', 'C#'),
|
|
|
|
|
('cpp', 'C++'),
|
|
|
|
|
('c', 'C'),
|
|
|
|
|
('go', 'Go'),
|
|
|
|
|
('rust', 'Rust'),
|
|
|
|
|
('ruby', 'Ruby'),
|
|
|
|
|
('php', 'PHP'),
|
|
|
|
|
('bash', 'Shell / Bash'),
|
|
|
|
|
('sql', 'SQL'),
|
|
|
|
|
('json', 'JSON'),
|
|
|
|
|
('yaml', 'YAML'),
|
|
|
|
|
('xml', 'XML / HTML'),
|
|
|
|
|
('css', 'CSS'),
|
|
|
|
|
('markdown', 'Markdown'),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<CodeEditor> createState() => _CodeEditorState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _CodeEditorState extends State<CodeEditor> {
|
|
|
|
|
late final TextEditingController _title;
|
|
|
|
|
late final TextEditingController _code;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_title = TextEditingController(text: widget.slide.title);
|
|
|
|
|
_title.addListener(
|
|
|
|
|
() => widget.onUpdate(widget.slide.copyWith(title: _title.text)),
|
|
|
|
|
);
|
|
|
|
|
_code = TextEditingController(text: widget.slide.customMarkdown);
|
|
|
|
|
_code.addListener(
|
|
|
|
|
() => widget.onUpdate(widget.slide.copyWith(customMarkdown: _code.text)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_title.dispose();
|
|
|
|
|
_code.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final l10n = context.l10n;
|
|
|
|
|
// Houd de huidige taal selecteerbaar, ook als die niet in de lijst staat.
|
|
|
|
|
final current = widget.slide.codeLanguage.trim();
|
|
|
|
|
final items = [
|
|
|
|
|
...CodeEditor._languages,
|
|
|
|
|
if (current.isNotEmpty &&
|
|
|
|
|
!CodeEditor._languages.any((e) => e.$1 == current))
|
|
|
|
|
(current, current),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
Add dual-screen presenter mode (slide on beamer, notes on laptop)
When a second display is connected (macOS), presenting now opens a
borderless audience window on the beamer showing the slide, while the
main window shows the presenter view (current/next slide, speaker notes,
clock, controls) on the laptop. The two windows stay in sync over method
channels: navigation, blank screen, audio-complete and beamer clicks are
forwarded between them, and media plays only on the beamer to avoid
double audio. Falls back to the existing single-window presenter when
there is one display or the second window can't be created.
- Vendors a fork of desktop_multi_window in third_party/ that re-adds the
native macOS window geometry/fullscreen calls (coverScreen, setFrame,
close) the published 0.3.0 dropped; wired via a path dependency.
- Registers the app's plugins for sub-windows in MainFlutterWindow so
video/image rendering works on the beamer.
- Routes the multi_window dart entrypoint to a minimal AudienceWindowApp.
Compiles (flutter analyze + macOS debug build) and all tests pass;
runtime two-screen behaviour still needs verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:25:34 +02:00
|
|
|
EditorField(label: 'Titel (optioneel)', controller: _title),
|
2026-06-06 20:41:24 +02:00
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
l10n.d('Programmeertaal'),
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: Color(0xFF64748B),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
DropdownButton<String>(
|
|
|
|
|
value: items.any((e) => e.$1 == current) ? current : '',
|
|
|
|
|
isDense: true,
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
|
|
|
|
style: const TextStyle(fontSize: 12, color: Color(0xFF0F172A)),
|
|
|
|
|
items: [
|
|
|
|
|
for (final (id, label) in items)
|
|
|
|
|
DropdownMenuItem(value: id, child: Text(label)),
|
|
|
|
|
],
|
|
|
|
|
onChanged: (id) {
|
|
|
|
|
if (id == null) return;
|
|
|
|
|
widget.onUpdate(widget.slide.copyWith(codeLanguage: id));
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Text(
|
|
|
|
|
l10n.d('Broncode'),
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: Color(0xFF64748B),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: TextField(
|
|
|
|
|
controller: _code,
|
|
|
|
|
maxLines: null,
|
|
|
|
|
expands: true,
|
|
|
|
|
textAlignVertical: TextAlignVertical.top,
|
|
|
|
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
hintText: l10n.d('Plak of typ hier je broncode...'),
|
|
|
|
|
alignLabelWithHint: true,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|