305 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/foundation.dart';
 | |
| import 'package:flutter/gestures.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hooks/flutter_hooks.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;
 | |
| 
 | |
|     // State management for lazy loading
 | |
|     final shouldLoad = useState(false);
 | |
|     final isLoading = useState(false);
 | |
| 
 | |
|     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.globe
 | |
|                         : Symbols.iframe,
 | |
|                     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,
 | |
|                     ),
 | |
|                   ),
 | |
|                   InkWell(
 | |
|                     child: Icon(
 | |
|                       Symbols.open_in_new,
 | |
|                       size: 16,
 | |
|                       color: colorScheme.onSurfaceVariant,
 | |
|                     ),
 | |
|                     onTap: () async {
 | |
|                       final uri = Uri.parse(embedView.uri);
 | |
|                       if (await canLaunchUrl(uri)) {
 | |
|                         await launchUrl(
 | |
|                           uri,
 | |
|                           mode: LaunchMode.externalApplication,
 | |
|                         );
 | |
|                       }
 | |
|                     },
 | |
|                   ),
 | |
|                 ],
 | |
|               ),
 | |
|             ),
 | |
| 
 | |
|             // WebView content with lazy loading
 | |
|             AspectRatio(
 | |
|               aspectRatio: embedView.aspectRatio ?? 1,
 | |
|               child:
 | |
|                   shouldLoad.value
 | |
|                       ? Stack(
 | |
|                         children: [
 | |
|                           InAppWebView(
 | |
|                             gestureRecognizers: {
 | |
|                               Factory<VerticalDragGestureRecognizer>(
 | |
|                                 () => VerticalDragGestureRecognizer(),
 | |
|                               ),
 | |
|                               Factory<HorizontalDragGestureRecognizer>(
 | |
|                                 () => HorizontalDragGestureRecognizer(),
 | |
|                               ),
 | |
|                               Factory<ScaleGestureRecognizer>(
 | |
|                                 () => ScaleGestureRecognizer(),
 | |
|                               ),
 | |
|                               Factory<TapGestureRecognizer>(
 | |
|                                 () => TapGestureRecognizer(),
 | |
|                               ),
 | |
|                             },
 | |
|                             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) {
 | |
|                               isLoading.value = true;
 | |
|                             },
 | |
|                             onLoadStop: (controller, url) async {
 | |
|                               isLoading.value = false;
 | |
|                               // 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) {
 | |
|                               isLoading.value = false;
 | |
|                             },
 | |
|                             onLoadHttpError: (
 | |
|                               controller,
 | |
|                               url,
 | |
|                               statusCode,
 | |
|                               description,
 | |
|                             ) {
 | |
|                               isLoading.value = false;
 | |
|                             },
 | |
|                             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}',
 | |
|                               );
 | |
|                             },
 | |
|                           ),
 | |
|                           if (isLoading.value)
 | |
|                             Container(
 | |
|                               color: colorScheme.surfaceContainerLowest,
 | |
|                               child: const Center(
 | |
|                                 child: CircularProgressIndicator(),
 | |
|                               ),
 | |
|                             ),
 | |
|                         ],
 | |
|                       )
 | |
|                       : GestureDetector(
 | |
|                         onTap: () {
 | |
|                           shouldLoad.value = true;
 | |
|                         },
 | |
|                         child: Container(
 | |
|                           color: colorScheme.surfaceContainerLowest,
 | |
|                           child: Column(
 | |
|                             mainAxisAlignment: MainAxisAlignment.center,
 | |
|                             children: [
 | |
|                               Icon(
 | |
|                                 Symbols.play_arrow,
 | |
|                                 fill: 1,
 | |
|                                 size: 48,
 | |
|                                 color: colorScheme.onSurfaceVariant.withOpacity(
 | |
|                                   0.6,
 | |
|                                 ),
 | |
|                               ),
 | |
|                               Text(
 | |
|                                 'viewEmbedLoadHint'.tr(),
 | |
|                                 style: theme.textTheme.bodyMedium?.copyWith(
 | |
|                                   color: colorScheme.onSurfaceVariant
 | |
|                                       .withOpacity(0.6),
 | |
|                                 ),
 | |
|                               ),
 | |
|                             ],
 | |
|                           ),
 | |
|                         ),
 | |
|                       ),
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   String _getUriDisplay(String uri) {
 | |
|     try {
 | |
|       final parsedUri = Uri.parse(uri);
 | |
|       return parsedUri.host.isNotEmpty ? parsedUri.host : uri;
 | |
|     } catch (e) {
 | |
|       return uri;
 | |
|     }
 | |
|   }
 | |
| }
 |