2026-06-02 23:28:39 +02:00
|
|
|
import 'dart:io';
|
2026-06-08 12:18:35 +02:00
|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
2026-06-02 23:28:39 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2026-06-07 11:42:44 +02:00
|
|
|
import 'package:fl_chart/fl_chart.dart';
|
2026-06-04 00:59:14 +02:00
|
|
|
import 'package:flutter_highlight/flutter_highlight.dart';
|
|
|
|
|
import 'package:flutter_highlight/themes/github.dart';
|
2026-06-06 20:41:24 +02:00
|
|
|
import 'package:flutter_highlight/themes/atom-one-dark.dart';
|
2026-06-04 00:59:14 +02:00
|
|
|
import 'package:flutter_math_fork/flutter_math.dart';
|
|
|
|
|
import 'package:highlight/highlight.dart' show highlight;
|
|
|
|
|
import 'package:highlight/languages/all.dart' show allLanguages;
|
2026-06-02 23:28:39 +02:00
|
|
|
import 'package:video_player/video_player.dart';
|
2026-06-05 19:14:54 +02:00
|
|
|
import '../../l10n/app_localizations.dart';
|
2026-06-07 11:42:44 +02:00
|
|
|
import '../../models/chart.dart';
|
2026-06-02 23:28:39 +02:00
|
|
|
import '../../models/deck.dart';
|
|
|
|
|
import '../../models/settings.dart';
|
|
|
|
|
import '../../models/slide.dart';
|
|
|
|
|
import '../../theme/app_theme.dart';
|
Split slide_preview.dart and app_shell.dart into part files (#1)
Break the two largest widget files into part/part-of libraries grouped by
concern, with no public API or behaviour change (private widgets keep working
because parts share the library namespace; all imports stay in the main file).
slide_preview.dart 4748 -> 426 lines + slides/previews/{text,bullets,
checklist,table,media,code,chart,overlays}.dart
app_shell.dart 1930 -> 996 lines + shell/{shell_actions,tab_bar,
welcome_screen,status_bar,shell_overlays}.dart
fullscreen_presenter.dart is intentionally left as-is: ~1.6k of its lines are a
single interactive _FullscreenPresenterState (38 setState calls), which a
mechanical split cannot reduce and extensions can't host (protected setState).
Shrinking it needs a behaviour-affecting sub-widget extraction, tracked
separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:16:49 +02:00
|
|
|
import '../../utils/log.dart';
|
2026-06-02 23:28:39 +02:00
|
|
|
import 'inline_markdown.dart';
|
|
|
|
|
|
Split slide_preview.dart and app_shell.dart into part files (#1)
Break the two largest widget files into part/part-of libraries grouped by
concern, with no public API or behaviour change (private widgets keep working
because parts share the library namespace; all imports stay in the main file).
slide_preview.dart 4748 -> 426 lines + slides/previews/{text,bullets,
checklist,table,media,code,chart,overlays}.dart
app_shell.dart 1930 -> 996 lines + shell/{shell_actions,tab_bar,
welcome_screen,status_bar,shell_overlays}.dart
fullscreen_presenter.dart is intentionally left as-is: ~1.6k of its lines are a
single interactive _FullscreenPresenterState (38 setState calls), which a
mechanical split cannot reduce and extensions can't host (protected setState).
Shrinking it needs a behaviour-affecting sub-widget extraction, tracked
separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:16:49 +02:00
|
|
|
// Slide preview widgets, split into part files by slide type for
|
|
|
|
|
// navigability. These parts share this library's imports and private scope.
|
|
|
|
|
part 'previews/text_previews.dart';
|
|
|
|
|
part 'previews/bullets_previews.dart';
|
|
|
|
|
part 'previews/checklist_previews.dart';
|
|
|
|
|
part 'previews/table_preview.dart';
|
|
|
|
|
part 'previews/media_previews.dart';
|
|
|
|
|
part 'previews/code_preview.dart';
|
|
|
|
|
part 'previews/chart_preview.dart';
|
|
|
|
|
part 'previews/overlays.dart';
|
|
|
|
|
|
2026-06-03 15:03:27 +02:00
|
|
|
/// Returns a TextStyle with the correct font. 'EB Garamond' is bundled with the
|
|
|
|
|
/// app (see pubspec.yaml); all other fonts resolve to system families.
|
2026-06-02 23:28:39 +02:00
|
|
|
TextStyle _applyFont(String font, TextStyle base) {
|
|
|
|
|
return base.copyWith(fontFamily: font);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Geeft de link-tap-handler door aan alle tekst in een slide, zonder die door
|
|
|
|
|
/// elke sub-widget heen te hoeven sleuren. Draagt ook of er een TLP-markering
|
|
|
|
|
/// rechtsonder staat, zodat bijschriften daarboven uitwijken.
|
|
|
|
|
class _SlideLinkScope extends InheritedWidget {
|
|
|
|
|
final void Function(String url)? onTapLink;
|
|
|
|
|
final bool hasBottomTlp;
|
|
|
|
|
const _SlideLinkScope({
|
|
|
|
|
required this.onTapLink,
|
|
|
|
|
this.hasBottomTlp = false,
|
|
|
|
|
required super.child,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
static void Function(String url)? of(BuildContext context) {
|
|
|
|
|
return context
|
|
|
|
|
.dependOnInheritedWidgetOfExactType<_SlideLinkScope>()
|
|
|
|
|
?.onTapLink;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool hasBottomTlpOf(BuildContext context) {
|
|
|
|
|
return context
|
|
|
|
|
.dependOnInheritedWidgetOfExactType<_SlideLinkScope>()
|
|
|
|
|
?.hasBottomTlp ??
|
|
|
|
|
false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool updateShouldNotify(_SlideLinkScope oldWidget) =>
|
|
|
|
|
oldWidget.onTapLink != onTapLink ||
|
|
|
|
|
oldWidget.hasBottomTlp != hasBottomTlp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tekst met inline-markdown (**vet**, *cursief*, `code`, ~~door~~, [link](url)).
|
|
|
|
|
/// Vervangt platte [Text] op alle inhoudsplekken van een slide.
|
|
|
|
|
Widget _md(
|
|
|
|
|
BuildContext context,
|
|
|
|
|
String text,
|
|
|
|
|
TextStyle style, {
|
|
|
|
|
required Color linkColor,
|
|
|
|
|
int? maxLines,
|
|
|
|
|
TextAlign textAlign = TextAlign.start,
|
|
|
|
|
TextOverflow overflow = TextOverflow.clip,
|
|
|
|
|
bool softWrap = true,
|
|
|
|
|
}) {
|
|
|
|
|
return InlineMarkdownText(
|
|
|
|
|
text,
|
|
|
|
|
style: style,
|
|
|
|
|
linkColor: linkColor,
|
|
|
|
|
onTapLink: _SlideLinkScope.of(context),
|
|
|
|
|
maxLines: maxLines,
|
|
|
|
|
textAlign: textAlign,
|
|
|
|
|
overflow: overflow,
|
|
|
|
|
softWrap: softWrap,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Color _hexColor(String hex) {
|
|
|
|
|
final cleaned = hex.replaceFirst('#', '');
|
|
|
|
|
final value = int.tryParse(
|
|
|
|
|
cleaned.length == 6 ? 'FF$cleaned' : cleaned,
|
|
|
|
|
radix: 16,
|
|
|
|
|
);
|
|
|
|
|
return Color(value ?? 0xFFFFFFFF);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EdgeInsets _logoSafeInsets(double w, ThemeProfile profile) {
|
|
|
|
|
if (profile.logoPath?.isEmpty ?? true) return EdgeInsets.zero;
|
2026-06-09 22:57:40 +02:00
|
|
|
// Reserve just enough to clear the logo plus a small margin (matching the
|
|
|
|
|
// split-layout reserve). A larger margin needlessly shrinks the text area.
|
|
|
|
|
final reserved = w * ((profile.logoSize + 24) / 1280);
|
2026-06-02 23:28:39 +02:00
|
|
|
if (profile.logoPosition.startsWith('top')) {
|
|
|
|
|
return EdgeInsets.only(top: reserved);
|
|
|
|
|
}
|
|
|
|
|
return EdgeInsets.only(bottom: reserved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EdgeInsets _splitTextLogoSafeInsets(double w, ThemeProfile profile) {
|
|
|
|
|
if (profile.logoPath?.isEmpty ?? true) return EdgeInsets.zero;
|
|
|
|
|
if (profile.logoPosition.endsWith('right')) return EdgeInsets.zero;
|
|
|
|
|
final reserved = w * ((profile.logoSize + 24) / 1280);
|
|
|
|
|
if (profile.logoPosition.startsWith('top')) {
|
|
|
|
|
return EdgeInsets.only(top: reserved);
|
|
|
|
|
}
|
|
|
|
|
return EdgeInsets.only(bottom: reserved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Renders a visual approximation of a Marp slide inside a 16:9 container.
|
|
|
|
|
/// All font sizes and paddings are proportional to the widget width so the
|
|
|
|
|
/// same widget works both as the full preview pane and as a tiny thumbnail.
|
|
|
|
|
/// Content that exceeds the slide height is scaled down proportionally via
|
|
|
|
|
/// FittedBox rather than clipped.
|
|
|
|
|
class SlidePreviewWidget extends StatelessWidget {
|
|
|
|
|
final Slide slide;
|
|
|
|
|
final String? projectPath;
|
|
|
|
|
final ThemeProfile themeProfile;
|
|
|
|
|
|
|
|
|
|
/// Het lettertype hoort bij de stijl (themeProfile), niet bij de app.
|
|
|
|
|
String get fontFamily => themeProfile.fontFamily;
|
|
|
|
|
|
|
|
|
|
/// Optioneel: maakt links in de tekst klikbaar (preview/presenter). In
|
|
|
|
|
/// thumbnails en bij export blijft dit null → links zijn alleen gestyled.
|
|
|
|
|
final void Function(String url)? onLinkTap;
|
|
|
|
|
|
|
|
|
|
/// 1-gebaseerd slidenummer en totaal, voor footer-paginanummers en de
|
|
|
|
|
/// {page}/{total}-tokens. Null → geen paginanummers.
|
|
|
|
|
final int? slideNumber;
|
|
|
|
|
final int? slideCount;
|
|
|
|
|
|
|
|
|
|
/// TLP-classificatie van de presentatie; getoond als markering op de slide.
|
|
|
|
|
final TlpLevel tlp;
|
|
|
|
|
|
|
|
|
|
/// Of audio/video op deze slide afgespeeld kan worden (de audioknop verschijnt
|
|
|
|
|
/// en video kan starten). Standaard uit — thumbnails en export spelen niets.
|
|
|
|
|
final bool enableMedia;
|
|
|
|
|
|
|
|
|
|
/// Of media automatisch start (audio-/video-autoplay). In de editor-preview
|
|
|
|
|
/// staat dit uit (handmatig starten); in de presenter aan.
|
|
|
|
|
final bool autoplayMedia;
|
|
|
|
|
|
2026-06-08 12:18:35 +02:00
|
|
|
/// Vergroot grafieklabels voor weergave op afstand in presentatiemodus.
|
|
|
|
|
final bool presentationMode;
|
|
|
|
|
|
2026-06-09 13:28:23 +02:00
|
|
|
/// Wijzigt tijdens het presenteren een checklistitem. [column] is 0 voor de
|
|
|
|
|
/// eerste/enkele lijst en 1 voor de rechterkolom.
|
|
|
|
|
final void Function(int column, int itemIndex)? onChecklistItemToggle;
|
|
|
|
|
|
2026-06-02 23:28:39 +02:00
|
|
|
/// Wordt aangeroepen wanneer de audio van deze slide klaar is (voor de
|
|
|
|
|
/// automatische modus van de presenter).
|
|
|
|
|
final VoidCallback? onAudioComplete;
|
|
|
|
|
|
2026-06-09 13:28:23 +02:00
|
|
|
/// Wordt aangeroepen wanneer de video van deze slide klaar is.
|
|
|
|
|
final VoidCallback? onVideoComplete;
|
|
|
|
|
|
2026-06-02 23:28:39 +02:00
|
|
|
const SlidePreviewWidget({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.slide,
|
|
|
|
|
this.projectPath,
|
|
|
|
|
this.themeProfile = const ThemeProfile(),
|
|
|
|
|
this.onLinkTap,
|
|
|
|
|
this.slideNumber,
|
|
|
|
|
this.slideCount,
|
|
|
|
|
this.tlp = TlpLevel.none,
|
|
|
|
|
this.enableMedia = false,
|
|
|
|
|
this.autoplayMedia = false,
|
2026-06-08 12:18:35 +02:00
|
|
|
this.presentationMode = false,
|
2026-06-09 13:28:23 +02:00
|
|
|
this.onChecklistItemToggle,
|
2026-06-02 23:28:39 +02:00
|
|
|
this.onAudioComplete,
|
2026-06-09 13:28:23 +02:00
|
|
|
this.onVideoComplete,
|
2026-06-02 23:28:39 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-06-05 19:14:54 +02:00
|
|
|
final hasBottomRightTlp =
|
|
|
|
|
tlp != TlpLevel.none &&
|
|
|
|
|
!((themeProfile.logoPath?.isNotEmpty == true && slide.showLogo) &&
|
|
|
|
|
themeProfile.logoPosition == 'bottom-right');
|
2026-06-02 23:28:39 +02:00
|
|
|
// Make the widget self-sufficient for text rendering. On screen it sits
|
|
|
|
|
// inside a Material (which supplies a clean DefaultTextStyle), but the
|
|
|
|
|
// export rasterizer mounts it in a bare Overlay subtree. Without an
|
|
|
|
|
// explicit DefaultTextStyle there, any Text that doesn't set its own color
|
|
|
|
|
// falls back to Flutter's broken default — red letters with a yellow
|
|
|
|
|
// underline — which is exactly what showed up in exports. Wrapping here
|
|
|
|
|
// guarantees identical results in the preview and the export.
|
Add image-library dedupe and untagged filter, UI text scaling, table paste
Image library:
- "Clean up duplicates" finds byte-identical images by md5, keeps one
file per group (preferring the most-used, then the oldest), merges
the tags/descriptions and captions of the copies, repoints slides in
open decks and in .md presentations on disk, and deletes the copies
after a confirmation that lists every group.
- A header toggle filters to images without tags/description, so it is
easy to see which ones still need attention.
- The delete warning now also lists presentations on disk that still
reference the image (marked "not open"), next to the open decks.
Editor and accessibility (already in tree):
- Interface text scaling up to 200%, keyboard-operable panel divider,
keyboard-first add-slide dialog, and screen-reader improvements.
- Paste a spreadsheet/CSV/markdown selection into a table cell to fill
the whole grid.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:36:44 +02:00
|
|
|
// The slide is a fixed 16:9 design surface whose sizes all derive from
|
|
|
|
|
// its width; interface text scaling must not reflow it (the auto-fit
|
|
|
|
|
// measuring assumes unscaled text), so the canvas opts out.
|
|
|
|
|
return MediaQuery.withNoTextScaling(
|
|
|
|
|
child: _ChecklistInteractionHost(
|
|
|
|
|
enabled: presentationMode && onChecklistItemToggle != null,
|
|
|
|
|
onToggle: onChecklistItemToggle,
|
|
|
|
|
child: Directionality(
|
|
|
|
|
textDirection: TextDirection.ltr,
|
|
|
|
|
child: DefaultTextStyle(
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: _hexColor(themeProfile.textColor),
|
|
|
|
|
decoration: TextDecoration.none,
|
|
|
|
|
fontWeight: FontWeight.normal,
|
|
|
|
|
fontStyle: FontStyle.normal,
|
|
|
|
|
),
|
|
|
|
|
child: _SlideLinkScope(
|
|
|
|
|
onTapLink: onLinkTap,
|
|
|
|
|
hasBottomTlp: hasBottomRightTlp,
|
|
|
|
|
child: _buildSlide(),
|
|
|
|
|
),
|
2026-06-09 13:28:23 +02:00
|
|
|
),
|
2026-06-02 23:28:39 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildSlide() {
|
|
|
|
|
return LayoutBuilder(
|
|
|
|
|
builder: (_, constraints) {
|
|
|
|
|
final w = constraints.maxWidth;
|
|
|
|
|
return AspectRatio(
|
|
|
|
|
aspectRatio: 16 / 9,
|
|
|
|
|
child: ClipRect(
|
|
|
|
|
child: Stack(
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
children: [
|
|
|
|
|
_buildContent(w),
|
|
|
|
|
_FooterOverlay(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
slideNumber: slideNumber,
|
|
|
|
|
slideCount: slideCount,
|
|
|
|
|
tlp: tlp,
|
|
|
|
|
),
|
|
|
|
|
if (tlp != TlpLevel.none)
|
2026-06-05 19:14:54 +02:00
|
|
|
_TlpOverlay(
|
|
|
|
|
tlp: tlp,
|
|
|
|
|
w: w,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
hasLogo:
|
|
|
|
|
themeProfile.logoPath?.isNotEmpty == true &&
|
|
|
|
|
slide.showLogo,
|
|
|
|
|
),
|
2026-06-02 23:28:39 +02:00
|
|
|
if (themeProfile.logoPath?.isNotEmpty == true && slide.showLogo)
|
|
|
|
|
_LogoOverlay(
|
|
|
|
|
logoPath: themeProfile.logoPath!,
|
|
|
|
|
projectPath: projectPath,
|
|
|
|
|
position: themeProfile.logoPosition,
|
|
|
|
|
size: w * (themeProfile.logoSize / 1280),
|
|
|
|
|
),
|
|
|
|
|
if (enableMedia && slide.audioPath.isNotEmpty)
|
|
|
|
|
_AudioPlayback(
|
|
|
|
|
audioPath: slide.audioPath,
|
|
|
|
|
projectPath: projectPath,
|
|
|
|
|
autoplay: autoplayMedia && slide.audioAutoplay,
|
|
|
|
|
onComplete: onAudioComplete,
|
|
|
|
|
w: w,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildContent(double w) {
|
|
|
|
|
switch (slide.type) {
|
|
|
|
|
case SlideType.title:
|
|
|
|
|
return _TitlePreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
projectPath: projectPath,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
|
|
|
|
case SlideType.section:
|
|
|
|
|
return _SectionPreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
|
|
|
|
case SlideType.bullets:
|
|
|
|
|
return _BulletsPreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
|
|
|
|
case SlideType.twoBullets:
|
|
|
|
|
return _TwoBulletsPreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
|
|
|
|
case SlideType.bulletsImage:
|
|
|
|
|
return _BulletsImagePreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
projectPath: projectPath,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
|
|
|
|
case SlideType.twoImages:
|
|
|
|
|
return _TwoImagesPreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
projectPath: projectPath,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
|
|
|
|
case SlideType.image:
|
|
|
|
|
return _ImagePreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
projectPath: projectPath,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
|
|
|
|
case SlideType.video:
|
|
|
|
|
return _VideoPreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
projectPath: projectPath,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
autoplay: autoplayMedia && slide.videoAutoplay,
|
2026-06-09 13:28:23 +02:00
|
|
|
onComplete: onVideoComplete,
|
2026-06-02 23:28:39 +02:00
|
|
|
);
|
|
|
|
|
case SlideType.quote:
|
|
|
|
|
return _QuotePreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
projectPath: projectPath,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
|
|
|
|
case SlideType.table:
|
|
|
|
|
return _TablePreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
|
|
|
|
case SlideType.freeMarkdown:
|
|
|
|
|
return _MarkdownPreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
2026-06-06 20:41:24 +02:00
|
|
|
case SlideType.code:
|
|
|
|
|
return _CodePreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
|
|
|
|
);
|
2026-06-07 11:42:44 +02:00
|
|
|
case SlideType.chart:
|
|
|
|
|
return _ChartPreview(
|
|
|
|
|
slide: slide,
|
|
|
|
|
w: w,
|
|
|
|
|
font: fontFamily,
|
|
|
|
|
profile: themeProfile,
|
2026-06-08 12:18:35 +02:00
|
|
|
presentationMode: presentationMode,
|
2026-06-07 11:42:44 +02:00
|
|
|
);
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Split slide_preview.dart and app_shell.dart into part files (#1)
Break the two largest widget files into part/part-of libraries grouped by
concern, with no public API or behaviour change (private widgets keep working
because parts share the library namespace; all imports stay in the main file).
slide_preview.dart 4748 -> 426 lines + slides/previews/{text,bullets,
checklist,table,media,code,chart,overlays}.dart
app_shell.dart 1930 -> 996 lines + shell/{shell_actions,tab_bar,
welcome_screen,status_bar,shell_overlays}.dart
fullscreen_presenter.dart is intentionally left as-is: ~1.6k of its lines are a
single interactive _FullscreenPresenterState (38 setState calls), which a
mechanical split cannot reduce and extensions can't host (protected setState).
Shrinking it needs a behaviour-affecting sub-widget extraction, tracked
separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:16:49 +02:00
|
|
|
String? _resolvePath(String path, String? projectPath) =>
|
|
|
|
|
resolveSlideAssetPath(path, projectPath);
|
2026-06-02 23:28:39 +02:00
|
|
|
|
Split slide_preview.dart and app_shell.dart into part files (#1)
Break the two largest widget files into part/part-of libraries grouped by
concern, with no public API or behaviour change (private widgets keep working
because parts share the library namespace; all imports stay in the main file).
slide_preview.dart 4748 -> 426 lines + slides/previews/{text,bullets,
checklist,table,media,code,chart,overlays}.dart
app_shell.dart 1930 -> 996 lines + shell/{shell_actions,tab_bar,
welcome_screen,status_bar,shell_overlays}.dart
fullscreen_presenter.dart is intentionally left as-is: ~1.6k of its lines are a
single interactive _FullscreenPresenterState (38 setState calls), which a
mechanical split cannot reduce and extensions can't host (protected setState).
Shrinking it needs a behaviour-affecting sub-widget extraction, tracked
separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:16:49 +02:00
|
|
|
/// Resolves an image/media path the way the slide renderer does, so callers
|
|
|
|
|
/// (e.g. the presenter, to precache) can point at the exact file that will be
|
|
|
|
|
/// displayed. Returns null for an empty path.
|
|
|
|
|
String? resolveSlideAssetPath(String path, String? projectPath) {
|
|
|
|
|
if (path.isEmpty) return null;
|
|
|
|
|
if (path.startsWith('/') || path.contains(':\\')) return path;
|
|
|
|
|
if (projectPath != null) return '$projectPath/$path';
|
|
|
|
|
return path;
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|
|
|
|
|
|
Split slide_preview.dart and app_shell.dart into part files (#1)
Break the two largest widget files into part/part-of libraries grouped by
concern, with no public API or behaviour change (private widgets keep working
because parts share the library namespace; all imports stay in the main file).
slide_preview.dart 4748 -> 426 lines + slides/previews/{text,bullets,
checklist,table,media,code,chart,overlays}.dart
app_shell.dart 1930 -> 996 lines + shell/{shell_actions,tab_bar,
welcome_screen,status_bar,shell_overlays}.dart
fullscreen_presenter.dart is intentionally left as-is: ~1.6k of its lines are a
single interactive _FullscreenPresenterState (38 setState calls), which a
mechanical split cannot reduce and extensions can't host (protected setState).
Shrinking it needs a behaviour-affecting sub-widget extraction, tracked
separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:16:49 +02:00
|
|
|
/// Footer onderaan een slide: vrije tekst (links) + paginanummers (rechts),
|
|
|
|
|
/// op basis van het stijlprofiel. Verborgen op titel-/sectieslides (daar is
|
|
|
|
|
/// een footer ongebruikelijk en valt 'ie weg tegen de donkere achtergrond).
|
|
|
|
|
/// Linkermarge waar de inhoud (bullets/tekst) van een slide begint. Wordt
|
|
|
|
|
/// gebruikt om een links-uitgelijnde footer ermee te laten uitlijnen, zodat het
|
|
|
|
|
/// geheel consistenter oogt. Moet overeenkomen met de `pad`-waarden van de
|
|
|
|
|
/// afzonderlijke slide-renderers hierboven.
|
|
|
|
|
double _contentLeftInset(Slide slide, double w) {
|
|
|
|
|
switch (slide.type) {
|
|
|
|
|
case SlideType.bullets:
|
|
|
|
|
case SlideType.freeMarkdown:
|
|
|
|
|
return w * 0.07;
|
|
|
|
|
case SlideType.code:
|
|
|
|
|
return w * 0.05;
|
|
|
|
|
case SlideType.chart:
|
|
|
|
|
return w * 0.06;
|
|
|
|
|
case SlideType.twoBullets:
|
|
|
|
|
return w * 0.065;
|
|
|
|
|
case SlideType.table:
|
|
|
|
|
return w * 0.06;
|
|
|
|
|
case SlideType.bulletsImage:
|
|
|
|
|
return w * 0.038;
|
|
|
|
|
case SlideType.quote:
|
|
|
|
|
return w * 0.08;
|
|
|
|
|
default:
|
|
|
|
|
// Beeld/video: geen tekstmarge om mee uit te lijnen.
|
|
|
|
|
return w * 0.04;
|
2026-06-09 13:28:23 +02:00
|
|
|
}
|
|
|
|
|
}
|