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.RECOMMENDED, allowsBackForwardNavigationGestures: false, allowsLinkPreview: false, isInspectable: false, ), 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; } } }