234 lines
9.5 KiB
Dart
234 lines
9.5 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.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;
|
|
}
|
|
}
|
|
}
|