✨ Post embed view rendering
This commit is contained in:
		
							
								
								
									
										233
									
								
								lib/widgets/post/embed_view_renderer.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								lib/widgets/post/embed_view_renderer.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,233 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_inappwebview/flutter_inappwebview.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
|  | ||||
| class EmbedViewRenderer extends HookConsumerWidget { | ||||
|   final SnPostEmbedView embedView; | ||||
|   final double? maxHeight; | ||||
|   final BorderRadius? borderRadius; | ||||
|  | ||||
|   const EmbedViewRenderer({ | ||||
|     super.key, | ||||
|     required this.embedView, | ||||
|     this.maxHeight = 400, | ||||
|     this.borderRadius, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final theme = Theme.of(context); | ||||
|     final colorScheme = theme.colorScheme; | ||||
|  | ||||
|     return Container( | ||||
|       constraints: BoxConstraints(maxHeight: maxHeight ?? 400), | ||||
|       decoration: BoxDecoration( | ||||
|         borderRadius: borderRadius ?? BorderRadius.circular(12), | ||||
|         border: Border.all( | ||||
|           color: colorScheme.outline.withOpacity(0.3), | ||||
|           width: 1, | ||||
|         ), | ||||
|         color: colorScheme.surfaceContainerLowest, | ||||
|       ), | ||||
|       child: ClipRRect( | ||||
|         borderRadius: borderRadius ?? BorderRadius.circular(12), | ||||
|         child: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: [ | ||||
|             // Header with embed info | ||||
|             Container( | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), | ||||
|               decoration: BoxDecoration( | ||||
|                 color: colorScheme.surfaceContainerHigh, | ||||
|                 border: Border( | ||||
|                   bottom: BorderSide( | ||||
|                     color: colorScheme.outline.withOpacity(0.2), | ||||
|                     width: 1, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   Icon( | ||||
|                     embedView.renderer == PostEmbedViewRenderer.webView | ||||
|                         ? Symbols.web | ||||
|                         : Symbols.web, | ||||
|                     size: 16, | ||||
|                     color: colorScheme.primary, | ||||
|                   ), | ||||
|                   const SizedBox(width: 8), | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       _getUriDisplay(embedView.uri), | ||||
|                       style: theme.textTheme.bodySmall?.copyWith( | ||||
|                         color: colorScheme.onSurfaceVariant, | ||||
|                       ), | ||||
|                       maxLines: 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ), | ||||
|                   ), | ||||
|                   IconButton( | ||||
|                     icon: Icon( | ||||
|                       Symbols.open_in_new, | ||||
|                       size: 16, | ||||
|                       color: colorScheme.onSurfaceVariant, | ||||
|                     ), | ||||
|                     onPressed: () async { | ||||
|                       final uri = Uri.parse(embedView.uri); | ||||
|                       if (await canLaunchUrl(uri)) { | ||||
|                         await launchUrl( | ||||
|                           uri, | ||||
|                           mode: LaunchMode.externalApplication, | ||||
|                         ); | ||||
|                       } | ||||
|                     }, | ||||
|                     padding: EdgeInsets.zero, | ||||
|                     constraints: const BoxConstraints(), | ||||
|                     visualDensity: VisualDensity.compact, | ||||
|                     tooltip: 'Open in browser', | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|  | ||||
|             // WebView content | ||||
|             AspectRatio( | ||||
|               aspectRatio: embedView.aspectRatio ?? 1, | ||||
|               child: InAppWebView( | ||||
|                 initialUrlRequest: URLRequest(url: WebUri(embedView.uri)), | ||||
|                 initialSettings: InAppWebViewSettings( | ||||
|                   javaScriptEnabled: true, | ||||
|                   mediaPlaybackRequiresUserGesture: false, | ||||
|                   allowsInlineMediaPlayback: true, | ||||
|                   useShouldOverrideUrlLoading: true, | ||||
|                   useOnLoadResource: true, | ||||
|                   supportZoom: false, | ||||
|                   useWideViewPort: false, | ||||
|                   loadWithOverviewMode: true, | ||||
|                   builtInZoomControls: false, | ||||
|                   displayZoomControls: false, | ||||
|                   minimumFontSize: 12, | ||||
|                   preferredContentMode: UserPreferredContentMode.MOBILE, | ||||
|                   allowsBackForwardNavigationGestures: false, | ||||
|                   allowsLinkPreview: false, | ||||
|                   isInspectable: false, | ||||
|                   applicationNameForUserAgent: 'Solian/3.0', | ||||
|                   userAgent: | ||||
|                       'Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36 Solian/3.0', | ||||
|                 ), | ||||
|                 onWebViewCreated: (controller) { | ||||
|                   // Configure webview settings | ||||
|                   controller.addJavaScriptHandler( | ||||
|                     handlerName: 'onHeightChanged', | ||||
|                     callback: (args) { | ||||
|                       // Handle dynamic height changes if needed | ||||
|                     }, | ||||
|                   ); | ||||
|                 }, | ||||
|                 onLoadStart: (controller, url) { | ||||
|                   // Handle load start | ||||
|                 }, | ||||
|                 onLoadStop: (controller, url) async { | ||||
|                   // Inject CSS to improve mobile display and remove borders | ||||
|                   await controller.evaluateJavascript( | ||||
|                     source: ''' | ||||
|                     // Remove unwanted elements | ||||
|                     var elements = document.querySelectorAll('nav, header, footer, .ads, .advertisement, .sidebar'); | ||||
|                     for (var i = 0; i < elements.length; i++) { | ||||
|                       elements[i].style.display = 'none'; | ||||
|                     } | ||||
|              | ||||
|                     // Remove borders from embedded content (YouTube, Vimeo, etc.) | ||||
|                     var iframes = document.querySelectorAll('iframe'); | ||||
|                     for (var i = 0; i < iframes.length; i++) { | ||||
|                       iframes[i].style.border = 'none'; | ||||
|                       iframes[i].style.borderRadius = '0'; | ||||
|                     } | ||||
|              | ||||
|                     // Remove borders from video elements | ||||
|                     var videos = document.querySelectorAll('video'); | ||||
|                     for (var i = 0; i < videos.length; i++) { | ||||
|                       videos[i].style.border = 'none'; | ||||
|                       videos[i].style.borderRadius = '0'; | ||||
|                     } | ||||
|              | ||||
|                     // Remove borders from any element that might have them | ||||
|                     var allElements = document.querySelectorAll('*'); | ||||
|                     for (var i = 0; i < allElements.length; i++) { | ||||
|                       if (allElements[i].style.border) { | ||||
|                         allElements[i].style.border = 'none'; | ||||
|                       } | ||||
|                     } | ||||
|              | ||||
|                     // Improve readability | ||||
|                     var body = document.body; | ||||
|                     body.style.fontSize = '14px'; | ||||
|                     body.style.lineHeight = '1.4'; | ||||
|                     body.style.margin = '0'; | ||||
|                     body.style.padding = '0'; | ||||
|              | ||||
|                     // Handle dynamic content | ||||
|                     var observer = new MutationObserver(function(mutations) { | ||||
|                       // Remove borders from newly added elements | ||||
|                       var newIframes = document.querySelectorAll('iframe'); | ||||
|                       for (var i = 0; i < newIframes.length; i++) { | ||||
|                         if (!newIframes[i].style.border || newIframes[i].style.border !== 'none') { | ||||
|                           newIframes[i].style.border = 'none'; | ||||
|                           newIframes[i].style.borderRadius = '0'; | ||||
|                         } | ||||
|                       } | ||||
|                       var newVideos = document.querySelectorAll('video'); | ||||
|                       for (var i = 0; i < newVideos.length; i++) { | ||||
|                         if (!newVideos[i].style.border || newVideos[i].style.border !== 'none') { | ||||
|                           newVideos[i].style.border = 'none'; | ||||
|                           newVideos[i].style.borderRadius = '0'; | ||||
|                         } | ||||
|                       } | ||||
|                       window.flutter_inappwebview.callHandler('onHeightChanged', document.body.scrollHeight); | ||||
|                     }); | ||||
|                     observer.observe(document.body, { childList: true, subtree: true }); | ||||
|                   ''', | ||||
|                   ); | ||||
|                 }, | ||||
|                 onLoadError: (controller, url, code, message) { | ||||
|                   // Handle load errors | ||||
|                 }, | ||||
|                 onLoadHttpError: (controller, url, statusCode, description) { | ||||
|                   // Handle HTTP errors | ||||
|                 }, | ||||
|                 shouldOverrideUrlLoading: (controller, navigationAction) async { | ||||
|                   final uri = navigationAction.request.url; | ||||
|                   if (uri != null && uri.toString() != embedView.uri) { | ||||
|                     // Open external links in browser | ||||
|                     // You might want to use url_launcher here | ||||
|                     return NavigationActionPolicy.CANCEL; | ||||
|                   } | ||||
|                   return NavigationActionPolicy.ALLOW; | ||||
|                 }, | ||||
|                 onProgressChanged: (controller, progress) { | ||||
|                   // Handle progress changes if needed | ||||
|                 }, | ||||
|                 onConsoleMessage: (controller, consoleMessage) { | ||||
|                   // Handle console messages for debugging | ||||
|                   debugPrint('WebView Console: ${consoleMessage.message}'); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   String _getUriDisplay(String uri) { | ||||
|     try { | ||||
|       final parsedUri = Uri.parse(uri); | ||||
|       return parsedUri.host.isNotEmpty ? parsedUri.host : uri; | ||||
|     } catch (e) { | ||||
|       return uri; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -21,6 +21,7 @@ import 'package:island/widgets/post/post_item_screenshot.dart'; | ||||
| import 'package:island/widgets/post/post_award_sheet.dart'; | ||||
| import 'package:island/widgets/post/post_pin_sheet.dart'; | ||||
| import 'package:island/widgets/post/post_shared.dart'; | ||||
| import 'package:island/widgets/post/embed_view_renderer.dart'; | ||||
| import 'package:island/widgets/safety/abuse_report_helper.dart'; | ||||
| import 'package:island/widgets/share/share_sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| @@ -549,6 +550,12 @@ class PostItem extends HookConsumerWidget { | ||||
|           translationSection: translationSection, | ||||
|           renderingPadding: renderingPadding, | ||||
|         ), | ||||
|         if (item.embedView != null) | ||||
|           EmbedViewRenderer( | ||||
|             embedView: item.embedView!, | ||||
|             maxHeight: 400, | ||||
|             borderRadius: BorderRadius.circular(12), | ||||
|           ).padding(horizontal: renderingPadding.horizontal, vertical: 8), | ||||
|         if (isShowReference) | ||||
|           ReferencedPostWidget(item: item, renderingPadding: renderingPadding), | ||||
|         if (item.repliesCount > 0 && isEmbedReply) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user