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
|
|
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter/services.dart';
|
Add annotation layer (laser, pen, highlighter) over slides
Draw on slides while presenting, kept as a layer fully separate from the
Marp content so the deck stays pure, portable Marp.
Presenter tools (D pen, T highlighter, E eraser, X laser, C clear, Esc
puts the tool away) with a small floating colour/tool bar shown only when
a tool is active. Strokes use coordinates normalized to the 16:9 slide so
they render identically on the laptop and the beamer; in dual-screen mode
the ink and the laser are mirrored live to the audience window.
Persistence (decoupled from the .md):
- In memory the layer is keyed by Slide.id (stable within a session).
- On disk it lives in a sidecar <name>.ink.json next to the deck and as a
separate entry inside the .ocideck package; the markdown is untouched.
- Because slide ids are regenerated on load, the sidecar anchors strokes
by order + a content fingerprint, re-attaching them after reordering and
dropping them when a slide's content changed.
- Deck.annotations carries the layer in memory but is never serialized to
markdown; deckProvider.setAnnotations keeps it out of undo/redo.
flutter analyze is clean, all tests pass (incl. new stroke/codec tests),
and the macOS debug build compiles. Drawing and live beamer sync still
need verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:14:51 +02:00
|
|
|
import '../../models/annotation.dart';
|
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
|
|
|
import '../../models/deck.dart';
|
|
|
|
|
import '../../models/settings.dart';
|
|
|
|
|
import '../../models/slide.dart';
|
|
|
|
|
import '../../services/markdown_service.dart';
|
|
|
|
|
import '../../utils/url_launcher_util.dart';
|
|
|
|
|
import '../slides/slide_preview.dart';
|
Add annotation layer (laser, pen, highlighter) over slides
Draw on slides while presenting, kept as a layer fully separate from the
Marp content so the deck stays pure, portable Marp.
Presenter tools (D pen, T highlighter, E eraser, X laser, C clear, Esc
puts the tool away) with a small floating colour/tool bar shown only when
a tool is active. Strokes use coordinates normalized to the 16:9 slide so
they render identically on the laptop and the beamer; in dual-screen mode
the ink and the laser are mirrored live to the audience window.
Persistence (decoupled from the .md):
- In memory the layer is keyed by Slide.id (stable within a session).
- On disk it lives in a sidecar <name>.ink.json next to the deck and as a
separate entry inside the .ocideck package; the markdown is untouched.
- Because slide ids are regenerated on load, the sidecar anchors strokes
by order + a content fingerprint, re-attaching them after reordering and
dropping them when a slide's content changed.
- Deck.annotations carries the layer in memory but is never serialized to
markdown; deckProvider.setAnnotations keeps it out of undo/redo.
flutter analyze is clean, all tests pass (incl. new stroke/codec tests),
and the macOS debug build compiles. Drawing and live beamer sync still
need verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:14:51 +02:00
|
|
|
import 'annotation_overlay.dart';
|
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
|
|
|
|
|
|
|
|
/// Channel the audience (beamer) window listens on for updates from the
|
|
|
|
|
/// presenter (laptop) window.
|
|
|
|
|
const audienceChannel = WindowMethodChannel(
|
|
|
|
|
'ocideck/audience',
|
|
|
|
|
mode: ChannelMode.unidirectional,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/// Channel the presenter window listens on; the audience window uses it to
|
|
|
|
|
/// forward navigation (clicks on the beamer) and audio-complete events.
|
|
|
|
|
const presenterChannel = WindowMethodChannel(
|
|
|
|
|
'ocideck/presenter',
|
|
|
|
|
mode: ChannelMode.unidirectional,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/// The app that runs inside the secondary (beamer) window. It only renders the
|
|
|
|
|
/// current slide fullscreen; the presenter window drives it via [audienceChannel].
|
|
|
|
|
class AudienceWindowApp extends StatefulWidget {
|
|
|
|
|
final Map<String, dynamic> args;
|
|
|
|
|
|
|
|
|
|
const AudienceWindowApp({super.key, required this.args});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<AudienceWindowApp> createState() => _AudienceWindowAppState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _AudienceWindowAppState extends State<AudienceWindowApp> {
|
|
|
|
|
List<Slide> _slides = const [];
|
|
|
|
|
ThemeProfile _theme = const ThemeProfile();
|
|
|
|
|
TlpLevel _tlp = TlpLevel.none;
|
|
|
|
|
String? _projectPath;
|
|
|
|
|
int _index = 0;
|
|
|
|
|
int _blank = 0; // 0 = none, 1 = black, 2 = white
|
|
|
|
|
|
Add annotation layer (laser, pen, highlighter) over slides
Draw on slides while presenting, kept as a layer fully separate from the
Marp content so the deck stays pure, portable Marp.
Presenter tools (D pen, T highlighter, E eraser, X laser, C clear, Esc
puts the tool away) with a small floating colour/tool bar shown only when
a tool is active. Strokes use coordinates normalized to the 16:9 slide so
they render identically on the laptop and the beamer; in dual-screen mode
the ink and the laser are mirrored live to the audience window.
Persistence (decoupled from the .md):
- In memory the layer is keyed by Slide.id (stable within a session).
- On disk it lives in a sidecar <name>.ink.json next to the deck and as a
separate entry inside the .ocideck package; the markdown is untouched.
- Because slide ids are regenerated on load, the sidecar anchors strokes
by order + a content fingerprint, re-attaching them after reordering and
dropping them when a slide's content changed.
- Deck.annotations carries the layer in memory but is never serialized to
markdown; deckProvider.setAnnotations keeps it out of undo/redo.
flutter analyze is clean, all tests pass (incl. new stroke/codec tests),
and the macOS debug build compiles. Drawing and live beamer sync still
need verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:14:51 +02:00
|
|
|
// Annotation layer, keyed by slide index (the beamer has no stable ids).
|
|
|
|
|
final Map<int, List<InkStroke>> _ink = {};
|
|
|
|
|
int? _laserIndex;
|
|
|
|
|
Offset? _laserPoint;
|
|
|
|
|
|
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
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
final markdown = widget.args['markdown'] as String? ?? '';
|
|
|
|
|
_projectPath = widget.args['projectPath'] as String?;
|
|
|
|
|
_index = (widget.args['index'] as num?)?.toInt() ?? 0;
|
|
|
|
|
final deck = MarkdownService().parseDeck(markdown);
|
|
|
|
|
_slides = deck?.slides ?? const [];
|
|
|
|
|
_theme = deck?.themeProfile ?? const ThemeProfile();
|
|
|
|
|
_tlp = deck?.tlp ?? TlpLevel.none;
|
Add annotation layer (laser, pen, highlighter) over slides
Draw on slides while presenting, kept as a layer fully separate from the
Marp content so the deck stays pure, portable Marp.
Presenter tools (D pen, T highlighter, E eraser, X laser, C clear, Esc
puts the tool away) with a small floating colour/tool bar shown only when
a tool is active. Strokes use coordinates normalized to the 16:9 slide so
they render identically on the laptop and the beamer; in dual-screen mode
the ink and the laser are mirrored live to the audience window.
Persistence (decoupled from the .md):
- In memory the layer is keyed by Slide.id (stable within a session).
- On disk it lives in a sidecar <name>.ink.json next to the deck and as a
separate entry inside the .ocideck package; the markdown is untouched.
- Because slide ids are regenerated on load, the sidecar anchors strokes
by order + a content fingerprint, re-attaching them after reordering and
dropping them when a slide's content changed.
- Deck.annotations carries the layer in memory but is never serialized to
markdown; deckProvider.setAnnotations keeps it out of undo/redo.
flutter analyze is clean, all tests pass (incl. new stroke/codec tests),
and the macOS debug build compiles. Drawing and live beamer sync still
need verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:14:51 +02:00
|
|
|
// Pre-existing strokes passed at creation, keyed by index.
|
|
|
|
|
final ink = widget.args['ink'];
|
|
|
|
|
if (ink is Map) {
|
|
|
|
|
ink.forEach((k, v) {
|
|
|
|
|
final i = int.tryParse('$k');
|
|
|
|
|
if (i != null && v is List) _ink[i] = decodeStrokes(v);
|
|
|
|
|
});
|
|
|
|
|
}
|
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
|
|
|
audienceChannel.setMethodCallHandler(_onPresenterCall);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
audienceChannel.setMethodCallHandler(null);
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<dynamic> _onPresenterCall(MethodCall call) async {
|
|
|
|
|
switch (call.method) {
|
|
|
|
|
case 'update':
|
|
|
|
|
final m = Map<String, dynamic>.from(call.arguments as Map);
|
|
|
|
|
if (!mounted) return null;
|
|
|
|
|
setState(() {
|
|
|
|
|
_index = (m['index'] as num?)?.toInt() ?? _index;
|
|
|
|
|
_blank = (m['blank'] as num?)?.toInt() ?? 0;
|
Add annotation layer (laser, pen, highlighter) over slides
Draw on slides while presenting, kept as a layer fully separate from the
Marp content so the deck stays pure, portable Marp.
Presenter tools (D pen, T highlighter, E eraser, X laser, C clear, Esc
puts the tool away) with a small floating colour/tool bar shown only when
a tool is active. Strokes use coordinates normalized to the 16:9 slide so
they render identically on the laptop and the beamer; in dual-screen mode
the ink and the laser are mirrored live to the audience window.
Persistence (decoupled from the .md):
- In memory the layer is keyed by Slide.id (stable within a session).
- On disk it lives in a sidecar <name>.ink.json next to the deck and as a
separate entry inside the .ocideck package; the markdown is untouched.
- Because slide ids are regenerated on load, the sidecar anchors strokes
by order + a content fingerprint, re-attaching them after reordering and
dropping them when a slide's content changed.
- Deck.annotations carries the layer in memory but is never serialized to
markdown; deckProvider.setAnnotations keeps it out of undo/redo.
flutter analyze is clean, all tests pass (incl. new stroke/codec tests),
and the macOS debug build compiles. Drawing and live beamer sync still
need verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:14:51 +02:00
|
|
|
_laserPoint = null; // laser never carries over to another slide
|
|
|
|
|
});
|
|
|
|
|
case 'ink':
|
|
|
|
|
final m = Map<String, dynamic>.from(call.arguments as Map);
|
|
|
|
|
final i = (m['index'] as num?)?.toInt();
|
|
|
|
|
if (i == null || !mounted) return null;
|
Add project docs, EUPL licence, and open-source licence check
Documentation & licensing:
- Add the EUPL-1.2 licence (LICENSE.md) and set the project licence; refresh
the README (name origin wink, updated feature list, documentation index).
- Add CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, CHANGELOG, AUTHORS, and
THIRD_PARTY_NOTICES, plus docs/ (ARCHITECTURE, BUILD, USER_GUIDE, SHORTCUTS,
LICENSE_COMPLIANCE) and .github/ (CI workflow, issue/PR templates).
- Bring docs/FILE_FORMAT.md in line with current behaviour (code & chart
slides, per-slide TLP comment, annotation .ink.json sidecar, chart data/ CSVs).
Open-source compliance:
- Add tool/check_licenses.dart and a `make licenses` target (wired into
check-full and CI) that verifies every resolved dependency uses a recognised
open-source licence. A scan of all 151 packages and bundled assets found only
OSI-approved licences.
Charts (Fase 1.1):
- Replace the chart CSV textarea with an in-app editable data grid (editable
series/labels/values, add/remove row & column, read-only when linked).
- Centralize the linked-CSV directory name (`data/`) in a shared constant.
Also normalize formatting repo-wide with `dart format` and fix one
curly-braces lint, so `make check` and CI are green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:19:56 +02:00
|
|
|
setState(
|
|
|
|
|
() => _ink[i] = decodeStrokes((m['strokes'] as List?) ?? const []),
|
|
|
|
|
);
|
Add annotation layer (laser, pen, highlighter) over slides
Draw on slides while presenting, kept as a layer fully separate from the
Marp content so the deck stays pure, portable Marp.
Presenter tools (D pen, T highlighter, E eraser, X laser, C clear, Esc
puts the tool away) with a small floating colour/tool bar shown only when
a tool is active. Strokes use coordinates normalized to the 16:9 slide so
they render identically on the laptop and the beamer; in dual-screen mode
the ink and the laser are mirrored live to the audience window.
Persistence (decoupled from the .md):
- In memory the layer is keyed by Slide.id (stable within a session).
- On disk it lives in a sidecar <name>.ink.json next to the deck and as a
separate entry inside the .ocideck package; the markdown is untouched.
- Because slide ids are regenerated on load, the sidecar anchors strokes
by order + a content fingerprint, re-attaching them after reordering and
dropping them when a slide's content changed.
- Deck.annotations carries the layer in memory but is never serialized to
markdown; deckProvider.setAnnotations keeps it out of undo/redo.
flutter analyze is clean, all tests pass (incl. new stroke/codec tests),
and the macOS debug build compiles. Drawing and live beamer sync still
need verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:14:51 +02:00
|
|
|
case 'laser':
|
|
|
|
|
final m = Map<String, dynamic>.from(call.arguments as Map);
|
|
|
|
|
final i = (m['index'] as num?)?.toInt();
|
|
|
|
|
final pt = m['point'] as List?;
|
|
|
|
|
if (!mounted) return null;
|
|
|
|
|
setState(() {
|
|
|
|
|
_laserIndex = i;
|
|
|
|
|
_laserPoint = pt == null
|
|
|
|
|
? null
|
|
|
|
|
: Offset((pt[0] as num).toDouble(), (pt[1] as num).toDouble());
|
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
|
|
|
});
|
|
|
|
|
case 'close':
|
|
|
|
|
try {
|
|
|
|
|
final self = await WindowController.fromCurrentEngine();
|
|
|
|
|
await self.close();
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _send(String method) {
|
|
|
|
|
// Best-effort: the presenter may already be gone.
|
|
|
|
|
presenterChannel.invokeMethod(method).catchError((_) => null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return MaterialApp(
|
|
|
|
|
debugShowCheckedModeBanner: false,
|
|
|
|
|
home: Scaffold(backgroundColor: Colors.black, body: _body()),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _body() {
|
|
|
|
|
if (_slides.isEmpty) return const SizedBox.shrink();
|
|
|
|
|
if (_blank != 0) {
|
|
|
|
|
return Container(color: _blank == 2 ? Colors.white : Colors.black);
|
|
|
|
|
}
|
|
|
|
|
final slide = _slides[_index.clamp(0, _slides.length - 1)];
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () => _send('next'),
|
|
|
|
|
onSecondaryTap: () => _send('prev'),
|
|
|
|
|
child: SizedBox.expand(child: _canvas(slide)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A 16:9 slide letterboxed to fit the screen, mirroring the presenter's view.
|
|
|
|
|
Widget _canvas(Slide slide) {
|
|
|
|
|
return LayoutBuilder(
|
|
|
|
|
builder: (_, constraints) {
|
|
|
|
|
final w = constraints.maxWidth;
|
|
|
|
|
final h = constraints.maxHeight;
|
|
|
|
|
const ratio = 16.0 / 9.0;
|
|
|
|
|
double slideW, slideH;
|
|
|
|
|
if (w / h > ratio) {
|
|
|
|
|
slideH = h;
|
|
|
|
|
slideW = h * ratio;
|
|
|
|
|
} else {
|
|
|
|
|
slideW = w;
|
|
|
|
|
slideH = w / ratio;
|
|
|
|
|
}
|
|
|
|
|
return Center(
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
width: slideW,
|
|
|
|
|
height: slideH,
|
Add annotation layer (laser, pen, highlighter) over slides
Draw on slides while presenting, kept as a layer fully separate from the
Marp content so the deck stays pure, portable Marp.
Presenter tools (D pen, T highlighter, E eraser, X laser, C clear, Esc
puts the tool away) with a small floating colour/tool bar shown only when
a tool is active. Strokes use coordinates normalized to the 16:9 slide so
they render identically on the laptop and the beamer; in dual-screen mode
the ink and the laser are mirrored live to the audience window.
Persistence (decoupled from the .md):
- In memory the layer is keyed by Slide.id (stable within a session).
- On disk it lives in a sidecar <name>.ink.json next to the deck and as a
separate entry inside the .ocideck package; the markdown is untouched.
- Because slide ids are regenerated on load, the sidecar anchors strokes
by order + a content fingerprint, re-attaching them after reordering and
dropping them when a slide's content changed.
- Deck.annotations carries the layer in memory but is never serialized to
markdown; deckProvider.setAnnotations keeps it out of undo/redo.
flutter analyze is clean, all tests pass (incl. new stroke/codec tests),
and the macOS debug build compiles. Drawing and live beamer sync still
need verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:14:51 +02:00
|
|
|
child: Stack(
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
children: [
|
|
|
|
|
SlidePreviewWidget(
|
|
|
|
|
slide: slide,
|
|
|
|
|
projectPath: _projectPath,
|
|
|
|
|
themeProfile: _theme,
|
|
|
|
|
onLinkTap: openExternalUrl,
|
|
|
|
|
slideNumber: _index + 1,
|
|
|
|
|
slideCount: _slides.length,
|
|
|
|
|
tlp: _tlp,
|
2026-06-08 12:18:35 +02:00
|
|
|
presentationMode: true,
|
Add annotation layer (laser, pen, highlighter) over slides
Draw on slides while presenting, kept as a layer fully separate from the
Marp content so the deck stays pure, portable Marp.
Presenter tools (D pen, T highlighter, E eraser, X laser, C clear, Esc
puts the tool away) with a small floating colour/tool bar shown only when
a tool is active. Strokes use coordinates normalized to the 16:9 slide so
they render identically on the laptop and the beamer; in dual-screen mode
the ink and the laser are mirrored live to the audience window.
Persistence (decoupled from the .md):
- In memory the layer is keyed by Slide.id (stable within a session).
- On disk it lives in a sidecar <name>.ink.json next to the deck and as a
separate entry inside the .ocideck package; the markdown is untouched.
- Because slide ids are regenerated on load, the sidecar anchors strokes
by order + a content fingerprint, re-attaching them after reordering and
dropping them when a slide's content changed.
- Deck.annotations carries the layer in memory but is never serialized to
markdown; deckProvider.setAnnotations keeps it out of undo/redo.
flutter analyze is clean, all tests pass (incl. new stroke/codec tests),
and the macOS debug build compiles. Drawing and live beamer sync still
need verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:14:51 +02:00
|
|
|
enableMedia: true,
|
|
|
|
|
autoplayMedia: true,
|
|
|
|
|
// Audio finishing on the beamer drives the presenter's
|
|
|
|
|
// auto-advance.
|
|
|
|
|
onAudioComplete: () => _send('audioComplete'),
|
|
|
|
|
),
|
|
|
|
|
AnnotationLayer(
|
|
|
|
|
strokes: _ink[_index] ?? const [],
|
|
|
|
|
interactive: false,
|
|
|
|
|
laserPoint: _laserIndex == _index ? _laserPoint : null,
|
|
|
|
|
),
|
|
|
|
|
],
|
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
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|