@@ -14,19 +14,41 @@ import 'package:url_launcher/url_launcher.dart' as launcher;
1414import 'mime_media_provider.dart' ;
1515
1616/// Viewer for mime message contents
17- class MimeMessageViewer extends StatefulWidget {
17+ class MimeMessageViewer extends StatelessWidget {
18+ /// The mime message that should be shown
1819 final MimeMessage mimeMessage;
20+
21+ /// The optional maximum width for inline images
1922 final int ? maxImageWidth;
23+
24+ /// Sets if the height of this view should be set automatically, this is required to be `true` when using the MimeMessageViewer in a scrollable view.
2025 final bool adjustHeight;
26+
2127 final bool blockExternalImages;
28+
29+ /// The default text that should be shown for empty messages.
2230 final String ? emptyMessageText;
31+
32+ /// Handler for mailto: links. Typically you will want to open a new compose view prepulated with a `MessageBuilder.prepareMailtoBasedMessage(uri,from)` instance.
2333 final Future Function (Uri mailto, MimeMessage mimeMessage)? mailtoDelegate;
34+
35+ /// Handler for showing the given media widget, typically in its own screen
2436 final Future Function (InteractiveMediaWidget mediaViewer)? showMediaDelegate;
37+
38+ /// Register this callback if you want a reference to the [InAppWebViewController] .
2539 final void Function (InAppWebViewController controller)? onWebViewCreated;
40+
41+ /// This callback will be called when the webview zooms out after loading, usually this is a sign that the user might want to zoom in again.
2642 final void Function (InAppWebViewController controller, double zoomFactor)?
2743 onZoomed;
44+
45+ /// Is notified about any errors that might occur
2846 final void Function (Object ? exception, StackTrace ? stackTrace)? onError;
2947
48+ /// With a builder you can take over the rendering for certain messages or mime types.
49+ final Widget ? Function (BuildContext context, MimeMessage mimeMessage)?
50+ builder;
51+
3052 /// Creates a new mime message viewer
3153 ///
3254 /// [mimeMessage] The message with loaded message contents.
@@ -39,6 +61,7 @@ class MimeMessageViewer extends StatefulWidget {
3961 /// Set the [onWebViewCreated] callback if you want a reference to the [InAppWebViewController] .
4062 /// Set the [onZoomed] callback if you want to be notified when the webview is zoomed out after loading.
4163 /// Set the [onError] callback in case you want to be notfied about processing errors such as format exceptions.
64+ /// With a [builder] you can take over the rendering for certain messages or mime types.
4265 MimeMessageViewer ({
4366 Key ? key,
4467 required this .mimeMessage,
@@ -51,14 +74,22 @@ class MimeMessageViewer extends StatefulWidget {
5174 this .onWebViewCreated,
5275 this .onZoomed,
5376 this .onError,
77+ this .builder,
5478 }) : super (key: key);
5579
5680 @override
57- State <MimeMessageViewer > createState () {
58- if (mimeMessage.mediaType.isImage == true ) {
59- return _ImageViewerState ();
81+ Widget build (BuildContext context) {
82+ final callback = builder;
83+ if (callback != null ) {
84+ final builtWidget = callback (context, mimeMessage);
85+ if (builtWidget != null ) {
86+ return builtWidget;
87+ }
88+ }
89+ if (mimeMessage.mediaType.isImage) {
90+ return _ImageMimeMessageViewer (config: this );
6091 } else {
61- return _HtmlViewerState ( );
92+ return _HtmlMimeMessageViewer (config : this );
6293 }
6394 }
6495}
@@ -81,7 +112,15 @@ class _HtmlGenerationResult {
81112 _HtmlGenerationResult .error (this .errorDetails) : this .html = null ;
82113}
83114
84- class _HtmlViewerState extends State <MimeMessageViewer > {
115+ class _HtmlMimeMessageViewer extends StatefulWidget {
116+ final MimeMessageViewer config;
117+ _HtmlMimeMessageViewer ({Key ? key, required this .config}) : super (key: key);
118+
119+ @override
120+ State <StatefulWidget > createState () => _HtmlViewerState ();
121+ }
122+
123+ class _HtmlViewerState extends State <_HtmlMimeMessageViewer > {
85124 String ? _htmlData;
86125 bool ? _wereExternalImagesBlocked;
87126 bool _isGenerating = false ;
@@ -92,20 +131,21 @@ class _HtmlViewerState extends State<MimeMessageViewer> {
92131
93132 @override
94133 void initState () {
95- generateHtml (widget.blockExternalImages);
134+ generateHtml (widget.config. blockExternalImages);
96135 super .initState ();
97136 }
98137
99138 void generateHtml (bool blockExternalImages) async {
100139 _wereExternalImagesBlocked = blockExternalImages;
101140 _isGenerating = true ;
102- _isHtmlMessage = widget.mimeMessage.hasPart (MediaSubtype .textHtml);
103- final args = _HtmlGenerationArguments (widget.mimeMessage,
104- blockExternalImages, widget.emptyMessageText, widget.maxImageWidth);
141+ final mimeMessage = widget.config.mimeMessage;
142+ _isHtmlMessage = mimeMessage.hasPart (MediaSubtype .textHtml);
143+ final args = _HtmlGenerationArguments (mimeMessage, blockExternalImages,
144+ widget.config.emptyMessageText, widget.config.maxImageWidth);
105145 final result = await compute (_generateHtmlImpl, args);
106146 _htmlData = result.html;
107147 if (_htmlData == null ) {
108- final onError = widget.onError;
148+ final onError = widget.config. onError;
109149 if (onError != null ) {
110150 onError (result.errorDetails, null );
111151 }
@@ -151,11 +191,11 @@ class _HtmlViewerState extends State<MimeMessageViewer> {
151191 if (_isGenerating) {
152192 return Container (child: CircularProgressIndicator ());
153193 }
154- if (widget.blockExternalImages != _wereExternalImagesBlocked) {
155- generateHtml (widget.blockExternalImages);
194+ if (widget.config. blockExternalImages != _wereExternalImagesBlocked) {
195+ generateHtml (widget.config. blockExternalImages);
156196 }
157197
158- if (widget.adjustHeight) {
198+ if (widget.config. adjustHeight) {
159199 final size = MediaQuery .of (context).size;
160200 return SizedBox (
161201 height: _webViewHeight ?? size.height,
@@ -191,9 +231,9 @@ class _HtmlViewerState extends State<MimeMessageViewer> {
191231 : AndroidForceDark .FORCE_DARK_OFF ,
192232 ),
193233 ),
194- onWebViewCreated: widget.onWebViewCreated,
234+ onWebViewCreated: widget.config. onWebViewCreated,
195235 onLoadStop: (controller, url) async {
196- if (widget.adjustHeight) {
236+ if (widget.config. adjustHeight) {
197237 var scrollHeight = (await controller.evaluateJavascript (
198238 source: 'document.body.scrollHeight' ));
199239 // print('scrollHeight: $scrollHeight');
@@ -210,7 +250,7 @@ class _HtmlViewerState extends State<MimeMessageViewer> {
210250 }
211251 await controller.zoomBy (zoomFactor: scale, iosAnimated: true );
212252 scrollHeight = (scrollHeight * scale).ceil ();
213- final callback = widget.onZoomed;
253+ final callback = widget.config. onZoomed;
214254 if (callback != null ) {
215255 callback (controller, scale);
216256 }
@@ -231,7 +271,7 @@ class _HtmlViewerState extends State<MimeMessageViewer> {
231271 action: PermissionRequestResponseAction .GRANT ),
232272 );
233273 },
234- gestureRecognizers: widget.adjustHeight
274+ gestureRecognizers: widget.config. adjustHeight
235275 ? {
236276 Factory <LongPressGestureRecognizer >(
237277 () => LongPressGestureRecognizer ()),
@@ -245,24 +285,27 @@ class _HtmlViewerState extends State<MimeMessageViewer> {
245285 Future <NavigationActionPolicy > shouldOverrideUrlLoading (
246286 InAppWebViewController controller, NavigationAction request) async {
247287 final requestUri = request.request.url! ;
248- if (widget.mailtoDelegate != null && requestUri.isScheme ('mailto' )) {
249- await widget.mailtoDelegate !(requestUri, widget.mimeMessage);
288+ final mimeMessage = widget.config.mimeMessage;
289+ final mailtoHandler = widget.config.mailtoDelegate;
290+ if (mailtoHandler != null && requestUri.isScheme ('mailto' )) {
291+ await mailtoHandler (requestUri, mimeMessage);
250292 return NavigationActionPolicy .CANCEL ;
251293 }
252294 if (requestUri.isScheme ('cid' ) || requestUri.isScheme ('fetch' )) {
253295 // show inline part:
254296 var cid = Uri .decodeComponent (requestUri.host);
255297 final part = requestUri.isScheme ('cid' )
256- ? widget. mimeMessage.getPartWithContentId (cid)
257- : widget. mimeMessage.getPart (cid);
298+ ? mimeMessage.getPartWithContentId (cid)
299+ : mimeMessage.getPart (cid);
258300 if (part != null ) {
259301 final mediaProvider =
260- MimeMediaProviderFactory .fromMime (widget. mimeMessage, part);
302+ MimeMediaProviderFactory .fromMime (mimeMessage, part);
261303 final mediaWidget = InteractiveMediaWidget (
262304 mediaProvider: mediaProvider,
263305 );
264- if (widget.showMediaDelegate != null ) {
265- widget.showMediaDelegate !(mediaWidget);
306+ final showMediaCallback = widget.config.showMediaDelegate;
307+ if (showMediaCallback != null ) {
308+ showMediaCallback (mediaWidget);
266309 } else {
267310 setState (() {
268311 _mediaView = mediaWidget;
@@ -281,20 +324,29 @@ class _HtmlViewerState extends State<MimeMessageViewer> {
281324 }
282325}
283326
327+ class _ImageMimeMessageViewer extends StatefulWidget {
328+ final MimeMessageViewer config;
329+
330+ const _ImageMimeMessageViewer ({Key ? key, required this .config})
331+ : super (key: key);
332+ @override
333+ State <StatefulWidget > createState () => _ImageViewerState ();
334+ }
335+
284336/// State for a message with `Content-Type: image/XXX`
285- class _ImageViewerState extends State <MimeMessageViewer > {
286- bool showFullScreen = false ;
287- Uint8List ? imageData ;
337+ class _ImageViewerState extends State <_ImageMimeMessageViewer > {
338+ bool _showFullScreen = false ;
339+ Uint8List ? _imageData ;
288340
289341 @override
290342 void initState () {
291- imageData = widget.mimeMessage.decodeContentBinary ();
343+ _imageData = widget.config .mimeMessage.decodeContentBinary ();
292344 super .initState ();
293345 }
294346
295347 @override
296348 Widget build (BuildContext context) {
297- if (showFullScreen ) {
349+ if (_showFullScreen ) {
298350 final screenHeight = MediaQuery .of (context).size.height;
299351 return WillPopScope (
300352 child: LayoutBuilder (
@@ -306,30 +358,31 @@ class _ImageViewerState extends State<MimeMessageViewer> {
306358 constraints: constraints,
307359 child: ImageInteractiveMedia (
308360 mediaProvider: MimeMediaProviderFactory .fromMime (
309- widget.mimeMessage, widget.mimeMessage)),
361+ widget.config. mimeMessage, widget.config .mimeMessage)),
310362 );
311363 },
312364 ),
313365 onWillPop: () {
314- setState (() => showFullScreen = false );
366+ setState (() => _showFullScreen = false );
315367 return Future .value (false );
316368 },
317369 );
318370 } else {
319371 return TextButton (
320372 onPressed: () {
321- if (widget.showMediaDelegate != null ) {
373+ final callback = widget.config.showMediaDelegate;
374+ if (callback != null ) {
322375 final mediaProvider = MimeMediaProviderFactory .fromMime (
323- widget.mimeMessage, widget.mimeMessage);
376+ widget.config. mimeMessage, widget.config .mimeMessage);
324377 final mediaWidget =
325378 InteractiveMediaWidget (mediaProvider: mediaProvider);
326- widget. showMediaDelegate ! (mediaWidget);
379+ callback (mediaWidget);
327380 } else {
328- setState (() => showFullScreen = true );
381+ setState (() => _showFullScreen = true );
329382 }
330383 },
331- child: imageData != null
332- ? Image .memory (imageData ! )
384+ child: _imageData != null
385+ ? Image .memory (_imageData ! )
333386 : Text ('no image data' ),
334387 );
335388 }
0 commit comments