Ocideck/lib/widgets/editors/code_editor.dart
Brenno de Winter 2aca44365a 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

142 lines
4.3 KiB
Dart

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: [
EditorField(label: 'Titel (optioneel)', controller: _title),
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,
),
),
),
],
),
);
}
}