From 3493cf8617d421cb5cd5a936ed9c0046735bd67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 23 Jul 2026 14:16:33 +0200 Subject: [PATCH 1/2] fixed error on pdf viewer transition --- .../marianum_message_view.dart | 13 ++-- .../ticker/widgets/ticker_page_body.dart | 5 +- .../file_viewer/deferred_pdf_viewer.dart | 45 ++------------ lib/widget/route_transition_gate.dart | 59 +++++++++++++++++++ 4 files changed, 76 insertions(+), 46 deletions(-) create mode 100644 lib/widget/route_transition_gate.dart diff --git a/lib/view/pages/marianum_message/marianum_message_view.dart b/lib/view/pages/marianum_message/marianum_message_view.dart index 9647aa1..68af85a 100644 --- a/lib/view/pages/marianum_message/marianum_message_view.dart +++ b/lib/view/pages/marianum_message/marianum_message_view.dart @@ -7,6 +7,7 @@ import '../../../api/marianumconnect/queries/get_newsletter_file/get_newsletter_ import '../../../widget/app_progress_indicator.dart'; import '../../../widget/confirm_dialog.dart'; import '../../../widget/placeholder_view.dart'; +import '../../../widget/route_transition_gate.dart'; class MessageView extends StatefulWidget { final String id; @@ -35,11 +36,13 @@ class _MessageViewState extends State { if (!snapshot.hasData) { return const Center(child: AppProgressIndicator.large()); } - return SfPdfViewer.memory( - snapshot.data!, - enableHyperlinkNavigation: true, - onHyperlinkClicked: (PdfHyperlinkClickedDetails e) => - ConfirmDialog.openBrowser(context, e.uri), + return RouteTransitionGate( + builder: (context) => SfPdfViewer.memory( + snapshot.data!, + enableHyperlinkNavigation: true, + onHyperlinkClicked: (PdfHyperlinkClickedDetails e) => + ConfirmDialog.openBrowser(context, e.uri), + ), ); }, ), diff --git a/lib/view/pages/ticker/widgets/ticker_page_body.dart b/lib/view/pages/ticker/widgets/ticker_page_body.dart index 0babaf6..86b9f13 100644 --- a/lib/view/pages/ticker/widgets/ticker_page_body.dart +++ b/lib/view/pages/ticker/widgets/ticker_page_body.dart @@ -17,6 +17,7 @@ import '../../../../theming/app_theme.dart'; import '../../../../widget/app_progress_indicator.dart'; import '../../../../widget/placeholder_view.dart'; import '../../../../widget/prosemirror/pm_json_view.dart'; +import '../../../../widget/route_transition_gate.dart'; import 'ticker_content_card.dart'; import 'ticker_updated_bar.dart'; @@ -187,7 +188,9 @@ class _ProxiedFileViewState extends State<_ProxiedFileView> { text: 'Das Dokument konnte nicht geladen werden.', ); } - return SfPdfViewer.memory(bytes); + return RouteTransitionGate( + builder: (context) => SfPdfViewer.memory(bytes), + ); }, ); } diff --git a/lib/widget/file_viewer/deferred_pdf_viewer.dart b/lib/widget/file_viewer/deferred_pdf_viewer.dart index a3428aa..5d63a40 100644 --- a/lib/widget/file_viewer/deferred_pdf_viewer.dart +++ b/lib/widget/file_viewer/deferred_pdf_viewer.dart @@ -3,51 +3,16 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; -import '../app_progress_indicator.dart'; +import '../route_transition_gate.dart'; /// SfPdfViewer asserts on `localToGlobal` if mounted during the page-push /// animation. Defer until the route enter animation completes. -class DeferredPdfViewer extends StatefulWidget { +class DeferredPdfViewer extends StatelessWidget { const DeferredPdfViewer({super.key, required this.path}); final String path; @override - State createState() => _DeferredPdfViewerState(); -} - -class _DeferredPdfViewerState extends State { - bool _ready = false; - Animation? _routeAnimation; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (_ready || _routeAnimation != null) return; - final animation = ModalRoute.of(context)?.animation; - if (animation == null || animation.isCompleted) { - _ready = true; - return; - } - _routeAnimation = animation..addStatusListener(_onAnimationStatus); - } - - void _onAnimationStatus(AnimationStatus status) { - if (status == AnimationStatus.completed && mounted) { - setState(() => _ready = true); - } - } - - @override - void dispose() { - _routeAnimation?.removeStatusListener(_onAnimationStatus); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!_ready) { - return const Center(child: AppProgressIndicator.large()); - } - return SfPdfViewer.file(File(widget.path)); - } + Widget build(BuildContext context) => RouteTransitionGate( + builder: (context) => SfPdfViewer.file(File(path)), + ); } diff --git a/lib/widget/route_transition_gate.dart b/lib/widget/route_transition_gate.dart new file mode 100644 index 0000000..f1648a9 --- /dev/null +++ b/lib/widget/route_transition_gate.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import 'app_progress_indicator.dart'; + +/// Delays building [builder] until the enclosing route's enter animation has +/// finished. Some widgets (notably `SfPdfViewer`) call `localToGlobal` during +/// their first layout and assert with `RenderBox was not laid out` when an +/// ancestor page-transition `RenderTransform` still has no size mid-push. +/// Gating the mount behind the settled animation avoids that race. +class RouteTransitionGate extends StatefulWidget { + const RouteTransitionGate({super.key, required this.builder, this.placeholder}); + + final WidgetBuilder builder; + + /// Shown while the route is still animating in. Defaults to a centered + /// large progress indicator. + final Widget? placeholder; + + @override + State createState() => _RouteTransitionGateState(); +} + +class _RouteTransitionGateState extends State { + bool _ready = false; + Animation? _routeAnimation; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_ready || _routeAnimation != null) return; + final animation = ModalRoute.of(context)?.animation; + if (animation == null || animation.isCompleted) { + _ready = true; + return; + } + _routeAnimation = animation..addStatusListener(_onAnimationStatus); + } + + void _onAnimationStatus(AnimationStatus status) { + if (status == AnimationStatus.completed && mounted) { + setState(() => _ready = true); + } + } + + @override + void dispose() { + _routeAnimation?.removeStatusListener(_onAnimationStatus); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!_ready) { + return widget.placeholder ?? + const Center(child: AppProgressIndicator.large()); + } + return widget.builder(context); + } +} From 624c5512a6b20713f49403bbb8162ae46db828b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 23 Jul 2026 14:22:32 +0200 Subject: [PATCH 2/2] fixed failing test --- test/widget/prosemirror/pm_table_view_test.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/widget/prosemirror/pm_table_view_test.dart b/test/widget/prosemirror/pm_table_view_test.dart index 12470b8..46ecf2a 100644 --- a/test/widget/prosemirror/pm_table_view_test.dart +++ b/test/widget/prosemirror/pm_table_view_test.dart @@ -81,7 +81,22 @@ void main() { testWidgets('tapping a toolbar button switches the shared mode', ( tester, ) async { - await tester.pumpWidget(_host(_tableDoc())); + // The mode toolbar only renders once the table overflows its width — at the + // full test surface the fixture table fits and the toggle stays hidden. Pin + // a narrow width so it overflows, then let the post-frame overflow + // measurement settle so the toolbar is present before tapping. + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: Center( + child: SizedBox(width: 200, child: PmDocumentView(doc: _tableDoc())), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); expect(pmTableMode.value, PmTableMode.scroll); await tester.tap(find.byTooltip('Spalten umbrechen').first);