package com.sample.content.kotlin.ui.components import android.annotation.SuppressLint import android.graphics.Bitmap import android.net.Uri import android.webkit.* import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("SetJavaScriptEnabled") @Composable fun FullScreenWebView(url: String, isConnected: Boolean = true) { val context = LocalContext.current var canGoBack by remember { mutableStateOf(false) } var webView: WebView? by remember { mutableStateOf(null) } var showNetworkError by remember { mutableStateOf(false) } var isLoading by remember { mutableStateOf(true) } var isRefreshing by remember { mutableStateOf(false) } // Extract main domain from initial URL for error filtering val mainDomain = remember(url) { try { val host = Uri.parse(url)?.host ?: "" if (host.matches(Regex("""^\d{1,3}(\.\d{1,3}){3}$"""))) { host // IP address - use as-is } else { val withoutWww = if (host.startsWith("www.")) host.substring(4) else host val parts = withoutWww.split(".") if (parts.size >= 2) "${parts[parts.size - 2]}.${parts[parts.size - 1]}" else withoutWww } } catch (e: Exception) { "" } } // Helper to check if request belongs to main domain fun isFromMainDomain(requestUrl: String?): Boolean { if (requestUrl.isNullOrBlank() || mainDomain.isBlank()) return false return try { val requestHost = Uri.parse(requestUrl)?.host ?: return false if (requestHost.matches(Regex("""^\d{1,3}(\.\d{1,3}){3}$"""))) { requestHost == mainDomain } else { val withoutWww = if (requestHost.startsWith("www.")) requestHost.substring(4) else requestHost withoutWww == mainDomain || withoutWww.endsWith(".$mainDomain") } } catch (e: Exception) { false } } // Handle back button BackHandler(enabled = canGoBack) { webView?.goBack() } // Auto-hide error when connection is restored LaunchedEffect(isConnected) { if (isConnected && showNetworkError) { showNetworkError = false webView?.reload() } } // Sync refresh indicator with page loading state LaunchedEffect(isLoading) { if (!isLoading && isRefreshing) { isRefreshing = false } } PullToRefreshBox( isRefreshing = isRefreshing, onRefresh = { if (!isConnected) { // Show error toast if offline Toast.makeText(context, "No internet connection", Toast.LENGTH_SHORT).show() isRefreshing = false } else { // Interrupt current load, clear cache, and reload isRefreshing = true webView?.apply { stopLoading() clearCache(true) reload() } } }, modifier = Modifier.fillMaxSize() ) { Box(modifier = Modifier.fillMaxSize()) { AndroidView( modifier = Modifier.fillMaxSize().systemBarsPadding(), factory = { ctx -> WebView(ctx).apply { webView = this // WebView settings settings.apply { javaScriptEnabled = true domStorageEnabled = true databaseEnabled = true setSupportZoom(true) builtInZoomControls = true displayZoomControls = false loadWithOverviewMode = true useWideViewPort = true javaScriptCanOpenWindowsAutomatically = true mediaPlaybackRequiresUserGesture = false allowFileAccess = true allowContentAccess = true setSupportMultipleWindows(true) // Enable mixed content mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW } // WebViewClient webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) canGoBack = view?.canGoBack() ?: false isLoading = true } override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) canGoBack = view?.canGoBack() ?: false isLoading = false } override fun onReceivedError( view: WebView?, request: WebResourceRequest?, error: WebResourceError? ) { super.onReceivedError(view, request, error) // Stop loading indicator isLoading = false // Silently ignore errors from third-party domains val requestUrl = request?.url?.toString() if (!isFromMainDomain(requestUrl)) { return } // Detect network-specific errors val isNetworkError = error?.errorCode in listOf( WebViewClient.ERROR_HOST_LOOKUP, WebViewClient.ERROR_CONNECT, WebViewClient.ERROR_TIMEOUT, WebViewClient.ERROR_IO, WebViewClient.ERROR_UNKNOWN ) // Show network error overlay only for main frame network errors from main domain if (isNetworkError && request?.isForMainFrame == true) { showNetworkError = true } // Sub-resource errors from main domain are silently ignored } } // WebChromeClient for advanced features webChromeClient = object : WebChromeClient() { // Geolocation permission override fun onGeolocationPermissionsShowPrompt( origin: String?, callback: GeolocationPermissions.Callback? ) { callback?.invoke(origin, true, false) } // Console messages for debugging override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { consoleMessage?.let { println("WebView Console: ${it.message()} -- From line ${it.lineNumber()} of ${it.sourceId()}") } return true } // Handle file chooser for uploads override fun onShowFileChooser( webView: WebView?, filePathCallback: ValueCallback>?, fileChooserParams: FileChooserParams? ): Boolean { // In a real app, launch file picker here // For now, just return false to use default behavior return false } } // Download listener setDownloadListener { url, userAgent, contentDisposition, mimeType, contentLength -> Toast.makeText( context, "Download started: $url", Toast.LENGTH_SHORT ).show() // In a real app, handle download here } // Load the URL loadUrl(url) } }, update = { view -> if (view.url != url) { view.loadUrl(url) } } ) // Network error overlay NetworkErrorOverlay( showError = showNetworkError, isConnected = isConnected, onRetry = { showNetworkError = false webView?.reload() }, modifier = Modifier.align(Alignment.TopCenter) ) // Loading progress bar AnimatedVisibility( visible = isLoading, enter = fadeIn(), exit = fadeOut(), modifier = Modifier .align(Alignment.TopCenter) .zIndex(1f) ) { LinearProgressIndicator( modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.primary ) } } } }