💄 Optimize embed view renderer loading logic

This commit is contained in:
2025-09-08 15:48:10 +08:00
parent 7cd10118cc
commit edb0a25f34

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
@@ -22,6 +23,10 @@ class EmbedViewRenderer extends HookConsumerWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
// State management for lazy loading
final shouldLoad = useState(false);
final isLoading = useState(false);
return Container( return Container(
constraints: BoxConstraints(maxHeight: maxHeight ?? 400), constraints: BoxConstraints(maxHeight: maxHeight ?? 400),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -93,125 +98,181 @@ class EmbedViewRenderer extends HookConsumerWidget {
), ),
), ),
// WebView content // WebView content with lazy loading
AspectRatio( AspectRatio(
aspectRatio: embedView.aspectRatio ?? 1, aspectRatio: embedView.aspectRatio ?? 1,
child: InAppWebView( child:
initialUrlRequest: URLRequest(url: WebUri(embedView.uri)), shouldLoad.value
initialSettings: InAppWebViewSettings( ? Stack(
javaScriptEnabled: true, children: [
mediaPlaybackRequiresUserGesture: false, InAppWebView(
allowsInlineMediaPlayback: true, initialUrlRequest: URLRequest(
useShouldOverrideUrlLoading: true, url: WebUri(embedView.uri),
useOnLoadResource: true, ),
supportZoom: false, initialSettings: InAppWebViewSettings(
useWideViewPort: false, javaScriptEnabled: true,
loadWithOverviewMode: true, mediaPlaybackRequiresUserGesture: false,
builtInZoomControls: false, allowsInlineMediaPlayback: true,
displayZoomControls: false, useShouldOverrideUrlLoading: true,
minimumFontSize: 12, useOnLoadResource: true,
preferredContentMode: UserPreferredContentMode.RECOMMENDED, supportZoom: false,
allowsBackForwardNavigationGestures: false, useWideViewPort: false,
allowsLinkPreview: false, loadWithOverviewMode: true,
isInspectable: false, builtInZoomControls: false,
), displayZoomControls: false,
onWebViewCreated: (controller) { minimumFontSize: 12,
// Configure webview settings preferredContentMode:
controller.addJavaScriptHandler( UserPreferredContentMode.RECOMMENDED,
handlerName: 'onHeightChanged', allowsBackForwardNavigationGestures: false,
callback: (args) { allowsLinkPreview: false,
// Handle dynamic height changes if needed isInspectable: false,
}, ),
); onWebViewCreated: (controller) {
}, // Configure webview settings
onLoadStart: (controller, url) { controller.addJavaScriptHandler(
// Handle load start handlerName: 'onHeightChanged',
}, callback: (args) {
onLoadStop: (controller, url) async { // Handle dynamic height changes if needed
// Inject CSS to improve mobile display and remove borders },
await controller.evaluateJavascript( );
source: ''' },
// Remove unwanted elements onLoadStart: (controller, url) {
var elements = document.querySelectorAll('nav, header, footer, .ads, .advertisement, .sidebar'); isLoading.value = true;
for (var i = 0; i < elements.length; i++) { },
elements[i].style.display = 'none'; onLoadStop: (controller, url) async {
} isLoading.value = false;
// Inject CSS to improve mobile display and remove borders
// Remove borders from embedded content (YouTube, Vimeo, etc.) await controller.evaluateJavascript(
var iframes = document.querySelectorAll('iframe'); source: '''
for (var i = 0; i < iframes.length; i++) { // Remove unwanted elements
iframes[i].style.border = 'none'; var elements = document.querySelectorAll('nav, header, footer, .ads, .advertisement, .sidebar');
iframes[i].style.borderRadius = '0'; for (var i = 0; i < elements.length; i++) {
} elements[i].style.display = 'none';
}
// Remove borders from video elements
var videos = document.querySelectorAll('video'); // Remove borders from embedded content (YouTube, Vimeo, etc.)
for (var i = 0; i < videos.length; i++) { var iframes = document.querySelectorAll('iframe');
videos[i].style.border = 'none'; for (var i = 0; i < iframes.length; i++) {
videos[i].style.borderRadius = '0'; iframes[i].style.border = 'none';
} iframes[i].style.borderRadius = '0';
}
// Remove borders from any element that might have them
var allElements = document.querySelectorAll('*'); // Remove borders from video elements
for (var i = 0; i < allElements.length; i++) { var videos = document.querySelectorAll('video');
if (allElements[i].style.border) { for (var i = 0; i < videos.length; i++) {
allElements[i].style.border = 'none'; videos[i].style.border = 'none';
} videos[i].style.borderRadius = '0';
} }
// Improve readability // Remove borders from any element that might have them
var body = document.body; var allElements = document.querySelectorAll('*');
body.style.fontSize = '14px'; for (var i = 0; i < allElements.length; i++) {
body.style.lineHeight = '1.4'; if (allElements[i].style.border) {
body.style.margin = '0'; allElements[i].style.border = 'none';
body.style.padding = '0'; }
}
// Handle dynamic content
var observer = new MutationObserver(function(mutations) { // Improve readability
// Remove borders from newly added elements var body = document.body;
var newIframes = document.querySelectorAll('iframe'); body.style.fontSize = '14px';
for (var i = 0; i < newIframes.length; i++) { body.style.lineHeight = '1.4';
if (!newIframes[i].style.border || newIframes[i].style.border !== 'none') { body.style.margin = '0';
newIframes[i].style.border = 'none'; body.style.padding = '0';
newIframes[i].style.borderRadius = '0';
} // Handle dynamic content
} var observer = new MutationObserver(function(mutations) {
var newVideos = document.querySelectorAll('video'); // Remove borders from newly added elements
for (var i = 0; i < newVideos.length; i++) { var newIframes = document.querySelectorAll('iframe');
if (!newVideos[i].style.border || newVideos[i].style.border !== 'none') { for (var i = 0; i < newIframes.length; i++) {
newVideos[i].style.border = 'none'; if (!newIframes[i].style.border || newIframes[i].style.border !== 'none') {
newVideos[i].style.borderRadius = '0'; newIframes[i].style.border = 'none';
} newIframes[i].style.borderRadius = '0';
} }
window.flutter_inappwebview.callHandler('onHeightChanged', document.body.scrollHeight); }
}); var newVideos = document.querySelectorAll('video');
observer.observe(document.body, { childList: true, subtree: true }); 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';
onLoadError: (controller, url, code, message) { }
// Handle load errors }
}, window.flutter_inappwebview.callHandler('onHeightChanged', document.body.scrollHeight);
onLoadHttpError: (controller, url, statusCode, description) { });
// Handle HTTP errors observer.observe(document.body, { childList: true, subtree: true });
}, ''',
shouldOverrideUrlLoading: (controller, navigationAction) async { );
final uri = navigationAction.request.url; },
if (uri != null && uri.toString() != embedView.uri) { onLoadError: (controller, url, code, message) {
// Open external links in browser isLoading.value = false;
// You might want to use url_launcher here },
return NavigationActionPolicy.CANCEL; onLoadHttpError: (
} controller,
return NavigationActionPolicy.ALLOW; url,
}, statusCode,
onProgressChanged: (controller, progress) { description,
// Handle progress changes if needed ) {
}, isLoading.value = false;
onConsoleMessage: (controller, consoleMessage) { },
// Handle console messages for debugging shouldOverrideUrlLoading: (
debugPrint('WebView Console: ${consoleMessage.message}'); 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,
size: 48,
color: colorScheme.onSurfaceVariant.withOpacity(
0.6,
),
),
const SizedBox(height: 8),
Text(
'Tap to load content',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant
.withOpacity(0.6),
),
),
],
),
),
),
), ),
], ],
), ),