🎨 Use feature based folder structure
This commit is contained in:
304
lib/posts/posts_widgets/post/embed_view_renderer.dart
Normal file
304
lib/posts/posts_widgets/post/embed_view_renderer.dart
Normal file
@@ -0,0 +1,304 @@
|
||||
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/posts/posts_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user