// Part of the slide_preview library — see ../slide_preview.dart. // Split out for navigability; all imports live in the main library file. part of '../slide_preview.dart'; class _LogoOverlay extends StatelessWidget { final String logoPath; final String? projectPath; final String position; final double size; const _LogoOverlay({ required this.logoPath, required this.projectPath, required this.position, required this.size, }); @override Widget build(BuildContext context) { final horizontalInset = size * 0.28; final topInset = size * 0.42; final bottomInset = size * 0.12; return Positioned( top: position.startsWith('top') ? topInset : null, bottom: position.startsWith('bottom') ? bottomInset : null, left: position.endsWith('left') ? horizontalInset : null, right: position.endsWith('right') ? horizontalInset : null, child: SizedBox( width: size, height: size, child: _resolvedImage( context, logoPath, projectPath, fit: BoxFit.contain, ), ), ); } } // ── TLP-markering: maten gedeeld door de badge en de footer-uitsparing ────── const double _kTlpFont = 0.018; // × slidebreedte const double _kTlpEdge = 0.025; // afstand tot de slidehoek (× breedte) const double _kTlpHPad = 0.011; const double _kTlpVPad = 0.005; double _tlpBottomInset(double w) => w * 0.022; /// Geschatte breedte van de TLP-badge, zodat de footer ervoor kan uitwijken. double _tlpBadgeWidth(double w, TlpLevel tlp) => tlp.label.length * w * _kTlpFont * 0.62 + 2 * (w * _kTlpHPad); /// Verticale ruimte die een TLP-badge rechtsonder inneemt (voor bijschriften). double _tlpVerticalReserve(double w) => w * _kTlpFont + 2 * (w * _kTlpVPad) + _tlpBottomInset(w); /// Officiële TLP 2.0-markering (FIRST): de gekleurde label op een zwart vlak, /// rechtsonder. Wijkt uit naar linksonder als het logo rechtsonder staat. class _TlpOverlay extends StatelessWidget { final TlpLevel tlp; final double w; final ThemeProfile profile; final bool hasLogo; const _TlpOverlay({ required this.tlp, required this.w, required this.profile, required this.hasLogo, }); @override Widget build(BuildContext context) { final toLeft = hasLogo && profile.logoPosition == 'bottom-right'; return Positioned( bottom: _tlpBottomInset(w), left: toLeft ? w * _kTlpEdge : null, right: toLeft ? null : w * _kTlpEdge, child: Container( padding: EdgeInsets.symmetric( horizontal: w * _kTlpHPad, vertical: w * _kTlpVPad, ), decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(w * 0.005), ), child: Text( tlp.label, style: TextStyle( color: Color(tlp.foreground), fontSize: w * _kTlpFont, fontWeight: FontWeight.w700, letterSpacing: 0.4, fontFamily: 'monospace', fontFamilyFallback: const ['Menlo', 'Consolas', 'Courier New'], height: 1.0, ), ), ), ); } } class _FooterOverlay extends StatelessWidget { final Slide slide; final double w; final ThemeProfile profile; final int? slideNumber; final int? slideCount; final TlpLevel tlp; const _FooterOverlay({ required this.slide, required this.w, required this.profile, this.slideNumber, this.slideCount, this.tlp = TlpLevel.none, }); String _applyTokens(String s) { final now = DateTime.now(); String two(int v) => v.toString().padLeft(2, '0'); final date = '${two(now.day)}-${two(now.month)}-${now.year}'; return s .replaceAll('{page}', slideNumber?.toString() ?? '') .replaceAll('{total}', slideCount?.toString() ?? '') .replaceAll('{date}', date) .replaceAll('{title}', slide.title); } @override Widget build(BuildContext context) { if (!slide.showFooter) return const SizedBox.shrink(); if (slide.type == SlideType.title || slide.type == SlideType.section) { return const SizedBox.shrink(); } final footerText = _applyTokens(profile.footerText).trim(); final showPages = profile.footerShowPageNumbers && slideNumber != null; if (footerText.isEmpty && !showPages) return const SizedBox.shrink(); // Iets kleiner dan voorheen, zodat 'ie niet tegen het logo aan drukt. final fontSize = w * 0.0145; final style = TextStyle( color: _hexColor(profile.textColor).withValues(alpha: 0.7), fontSize: fontSize, // Lichte schaduw zodat de footer ook over een afbeelding leesbaar blijft. shadows: [ Shadow( color: Colors.white.withValues(alpha: 0.5), blurRadius: w * 0.003, ), ], ); // Onderhoeken vrijhouden: de footer wijkt horizontaal uit voor het logo en // de TLP-badge, zodat ze netjes uitlijnen i.p.v. over elkaar te vallen. double mx(double a, double b) => a > b ? a : b; final hasLogo = profile.logoPath?.isNotEmpty == true && slide.showLogo; final logoBottom = hasLogo && profile.logoPosition.startsWith('bottom'); final logoOnLeft = profile.logoPosition.endsWith('left'); final logoSpan = w * (profile.logoSize / 1280) * 1.28 + w * 0.012; final logoLeftEdge = w * (profile.logoSize / 1280) * 0.28; final tlpOnRight = !(hasLogo && profile.logoPosition == 'bottom-right'); final tlpSpan = tlp == TlpLevel.none ? 0.0 : w * _kTlpEdge + _tlpBadgeWidth(w, tlp) + w * 0.012; final footerLeftAligned = profile.footerPosition == 'left'; // Links uitgelijnd begint de footer waar het logo of de bullets beginnen, // voor een consistente linkermarge. Anders de standaardmarge. var left = footerLeftAligned ? (logoBottom && logoOnLeft ? logoLeftEdge : _contentLeftInset(slide, w)) : w * 0.04; var right = w * 0.04; if (logoBottom) { if (logoOnLeft) { // Een links-uitgelijnde footer mag bewust met de logo-linkerkant // uitlijnen; anders schuift 'ie rechts van het logo om overlap te // voorkomen. if (!footerLeftAligned) left = mx(left, logoSpan); } else { right = mx(right, logoSpan); } } if (tlp != TlpLevel.none) { if (tlpOnRight) { right = mx(right, tlpSpan); } else { left = mx(left, tlpSpan); } } final alignment = switch (profile.footerPosition) { 'left' => Alignment.centerLeft, 'center' => Alignment.center, _ => Alignment.centerRight, }; final textAlign = switch (profile.footerPosition) { 'left' => TextAlign.left, 'center' => TextAlign.center, _ => TextAlign.right, }; return Positioned( left: left, right: right, bottom: w * 0.02, child: Align( alignment: alignment, child: ConstrainedBox( constraints: BoxConstraints(maxWidth: w - left - right), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (footerText.isNotEmpty) Flexible( child: Text( footerText, style: style, textAlign: textAlign, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (footerText.isNotEmpty && showPages) SizedBox(width: w * 0.02), if (showPages) Text( '$slideNumber / ${slideCount ?? slideNumber}', style: style, ), ], ), ), ), ); } }