init static method

WebViewController init({
  1. required bool isTour,
})

✅ Initialize WebView with full auto-size and click-channel support

Implementation

static WebViewController init({required bool isTour}) {
  final c = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setBackgroundColor(const Color(0x00000000))
    ..enableZoom(false);

  c.setNavigationDelegate(
    NavigationDelegate(
      onProgress: (_) {},
      onPageStarted: (_) {},
      onPageFinished: (String url) async {
        try {
          print("✅ onPageFinished: $url");
          await Future.delayed(const Duration(milliseconds: 400));

          // Inject button click listeners
          await c.runJavaScript('''
    document.querySelectorAll('button[data-action]').forEach(btn => {
      btn.addEventListener('click', (event) => {
        const action = btn.getAttribute('data-action');
        const anchor = btn.closest('a');
        let payload = { action };
        if (action === 'link' && anchor) {
          event.preventDefault();
          payload.url = anchor.href;
        }
        FlutterChannel.postMessage(JSON.stringify(payload));
      });
    });
  ''');

          // --- Safe JSON parser (no double decode) ---
          dynamic safeJsonParse(dynamic jsResult) {
            if (jsResult == null) return {};
            dynamic str = jsResult.toString().trim();

            // Unwrap quoted JSON like "\"{...}\""
            if (str.startsWith('"') && str.endsWith('"')) {
              str = str.substring(1, str.length - 1);
            }

            // Replace escaped quotes
            str = str.replaceAll(r'\"', '"');

            try {
              final json = jsonDecode(str);
              if (json is Map) return json;
            } catch (_) {}
            return {};
          }

          Map<String, double> newSize = {"height": 400, "width": 400};

          for (int attempt = 0; attempt < 3; attempt++) {
            final jsResult = await c
                .runJavaScriptReturningResult(calculateHtmlDocDimensions);

            if (jsResult.toString().contains('-1')) {
              await Future.delayed(const Duration(milliseconds: 300));
              continue;
            }

            final decoded = safeJsonParse(jsResult);

            final heightVal =
                double.tryParse(decoded['height']?.toString() ?? '0') ?? 0;
            final widthVal =
                double.tryParse(decoded['width']?.toString() ?? '0') ?? 0;

            // Pixel ratio
            final pixelRatioJs = await c
                .runJavaScriptReturningResult('window.devicePixelRatio');
            final pixelRatio =
                double.tryParse(pixelRatioJs.toString()) ?? 1.0;

            final adjustedHeight = (heightVal / pixelRatio);
            final adjustedWidth = (widthVal / pixelRatio);

            // Sanity cap
            if (adjustedHeight > 3000 || adjustedHeight < 100) {
              print("⚠️ Height suspicious ($adjustedHeight), fallback used");
              newSize = {"height": 400, "width": 400};
            } else {
              newSize = {"height": adjustedHeight, "width": adjustedWidth};
            }
            break;
          }

          if (newSize["height"]! > 5000) newSize["height"] = 500;

          sizeNotifier.value = newSize;
          print('📏 Final WebView size (adjusted): $newSize');
        } catch (e, st) {
          print('❌ Error calculating WebView size: $e\n$st');
          sizeNotifier.value = {"height": 400, "width": 400};
        }
      },
      onHttpError: (error) => print("HTTP Error: $error"),
      onWebResourceError: (error) => print("Web Resource Error: $error"),
      onNavigationRequest: (NavigationRequest request) {
        return NavigationDecision.navigate;
      },
    ),
  );

  if (!isTour) controller = c;
  return c;
}