Files
App/lib/widgets/post/embed_view_renderer.dart
2025-09-08 02:45:20 +08:00

231 lines
9.3 KiB
Dart

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