import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import '../../../../api/errors/error_mapper.dart'; import '../../../../api/errors/ticker_content_unavailable_exception.dart'; import '../../../../api/marianumconnect/queries/get_ticker_page/get_ticker_page_response.dart'; import '../../../../routing/app_routes.dart'; import '../../../../state/app/modules/ticker/repository/ticker_repository.dart'; import '../../../../theming/app_theme.dart'; import '../../../../widget/placeholder_view.dart'; import '../../../../widget/prosemirror/pm_json_view.dart'; import 'ticker_content_card.dart'; /// Embeddable renderer for a single ticker page. Loads the page on demand and /// renders it by kind: CONTENT via the native ProseMirror renderer, /// PROXIED_FILE as a PDF. It carries no Scaffold/AppBar so it can live both /// in-place inside [TickerView] and inside the standalone `TickerPageView` /// (deep links from outside the ticker module). /// /// Reaching a REDIRECT here (e.g. via an internal content link whose slug turns /// out to be a redirect) opens the browser and, if given, invokes [onRedirect] /// so the host can leave this page. class TickerPageBody extends StatefulWidget { final String slug; final void Function(String href) onLinkTap; final VoidCallback? onRedirect; const TickerPageBody({ super.key, required this.slug, required this.onLinkTap, this.onRedirect, }); @override State createState() => _TickerPageBodyState(); } class _TickerPageBodyState extends State { final TickerRepository _repo = TickerRepository(); late Future _future; @override void initState() { super.initState(); _future = _repo.getPage(widget.slug); } void _reload() { setState(() => _future = _repo.getPage(widget.slug)); } @override Widget build(BuildContext context) => FutureBuilder( future: _future, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } final error = snapshot.error; if (error != null) return _buildError(context, error); return _buildContent(context, snapshot.data!); }, ); Widget _buildError(BuildContext context, Object error) { if (error is TickerContentUnavailableException) { return PlaceholderView( icon: Icons.public_off_outlined, text: error.userMessage, button: error.webUrl == null ? null : ElevatedButton.icon( onPressed: () => AppRoutes.openWebUrl(error.webUrl!), icon: const Icon(Icons.open_in_new), label: const Text('Im Browser öffnen'), ), ); } return PlaceholderView( icon: Icons.error_outline, text: errorToUserMessage(error), button: errorAllowsRetry(error) ? ElevatedButton.icon( onPressed: _reload, icon: const Icon(Icons.refresh), label: const Text('Erneut versuchen'), ) : null, ); } Widget _buildContent(BuildContext context, TickerPageResponse page) { switch (page.kind) { case TickerPageKind.redirect: final url = page.externalUrl; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (url != null && url.isNotEmpty) AppRoutes.openExternalUrl(url); widget.onRedirect?.call(); }); return const Center(child: CircularProgressIndicator()); case TickerPageKind.proxiedFile: return _ProxiedFileView(repo: _repo, slug: widget.slug); default: final content = page.content; if (content == null) { return PlaceholderView( icon: Icons.public_off_outlined, text: 'Dieser Inhalt ist in der App nicht verfügbar.', button: page.webUrl == null ? null : ElevatedButton.icon( onPressed: () => AppRoutes.openWebUrl(page.webUrl!), icon: const Icon(Icons.open_in_new), label: const Text('Im Browser öffnen'), ), ); } return SingleChildScrollView( padding: const EdgeInsets.only(bottom: AppSpacing.lg), child: TickerContentCard( child: PmJsonView(json: content, onLinkTap: widget.onLinkTap), ), ); } } } class _ProxiedFileView extends StatefulWidget { final TickerRepository repo; final String slug; const _ProxiedFileView({required this.repo, required this.slug}); @override State<_ProxiedFileView> createState() => _ProxiedFileViewState(); } class _ProxiedFileViewState extends State<_ProxiedFileView> { late Future _bytes; @override void initState() { super.initState(); _bytes = widget.repo.getPageFile(widget.slug); } @override Widget build(BuildContext context) => FutureBuilder( future: _bytes, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } final error = snapshot.error; if (error != null) { return PlaceholderView( icon: Icons.error_outline, text: errorToUserMessage(error), button: errorAllowsRetry(error) ? ElevatedButton.icon( onPressed: () => setState( () => _bytes = widget.repo.getPageFile(widget.slug), ), icon: const Icon(Icons.refresh), label: const Text('Erneut versuchen'), ) : null, ); } final bytes = snapshot.data!; if (bytes.isEmpty) { return const PlaceholderView( icon: Icons.picture_as_pdf_outlined, text: 'Das Dokument konnte nicht geladen werden.', ); } return SfPdfViewer.memory(bytes); }, ); }