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;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |