Skip to content

Conversation

@ShashwatXD
Copy link
Contributor

@ShashwatXD ShashwatXD commented Dec 31, 2025

Fixes #388

What this PR does

Earlier, IB pages were rendered using Flutter Markdown(depreciated) with native HTML transformations, which caused issues with interactive content like quizzes and embeds.

This PR switches the content rendering to WebView, ensuring correct HTML behavior while keeping the UX clean.

Key changes:

  • Migrated content rendering from Flutter Markdown to WebView
  • Removed unwanted elements (navigation buttons, comments, sidebars, etc.) using injected CSS/JS
  • Preserved TOC navigation and page controls on the Flutter side
  • Internal links load inside WebView; external links open in browser
  • Fixed broken quizzes and other interactive elements

Video

Record_2026-01-01-02-20-00.mp4

Summary by CodeRabbit

  • New Features

    • Pages now render via an embedded WebView by default, with a toggle for the WebView path.
    • Embedded browser handles page loading, navigation, and TOC-driven in-page jumps.
  • UI Improvements

    • Floating controls auto-hide after inactivity and reappear on interaction.
    • Loading indicator, retry UI for load errors, and suppression of page chrome/comments for cleaner reading.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

Walkthrough

Replaces in-page Markdown/HTML rendering with a WebView-centric rendering path: introduces a WebViewController and navigation delegate (including URL filtering and external-launch handling), injects CSS/JS (including a MutationObserver) to remove page chrome and disable comments, and moves TOC/scroll behavior to JavaScript-driven navigation. Adds WebView loading state variables (_isLoading, _currentUrl), a webView error path and retry UI, a timer-driven FAB visibility model, an IbInteractionChannel for host–WebView interactions, and lifecycle cleanup. Removes Markdown-specific builders, scroll controllers, and related rendering/dependency code.

Possibly related PRs

  • CircuitVerse/mobile-app PR 304: Modifies the same lib/ui/views/ib/ib_page_view.dart and updates page-loading UI/logic, indicating direct overlap in page-rendering and loading-state changes.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix interactive books page view' is related to the main change but generic, referring to a bug fix without specifying the concrete solution (WebView migration).
Linked Issues check ✅ Passed The PR successfully addresses issue #388 by migrating content rendering to WebView, fixing parsing errors, restoring interactive elements (quizzes, embeds), and ensuring correct HTML behavior.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the interactive books page rendering issue, focusing on WebView migration, CSS/JS injection, navigation handling, and removal of deprecated Markdown rendering.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
lib/ui/views/ib/ib_page_view.dart (3)

157-160: Multiple setTimeout calls indicate timing issues.

Running cleanPage() four times at different intervals (immediately, 50ms, 200ms, 500ms) suggests the code is working around unpredictable page load timing. This is fragile and may not work reliably across different network conditions or page complexities.

Consider these alternatives:

  • Use DOMContentLoaded or framework-specific load events to trigger cleanup once
  • Combine with the MutationObserver (already present at line 162) as the primary mechanism
  • Use requestAnimationFrame for a single deferred cleanup after initial render

210-220: Consider adding error handling for JavaScript execution.

The JavaScript-based scrolling is appropriate for WebView, but there's no error handling if runJavaScript fails. While the JS itself logs when an element isn't found, the runJavaScript call itself could fail.

🔎 Optional: Add error handling
 Future _scrollToWidget(String slug) async {
   final jsCode = '''
     var element = document.getElementById('$slug');
     if (element) {
       element.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});
     } else {
       console.log("Element with id $slug not found");
     }
   ''';
-  await _webViewController.runJavaScript(jsCode);
+  try {
+    await _webViewController.runJavaScript(jsCode);
+  } catch (e) {
+    debugPrint('Failed to scroll to widget: $e');
+  }
 }

443-462: Add error state handling for WebView failures.

The current implementation shows a loading indicator when model.pageData == null or _isLoading == true, but doesn't handle WebView load failures reported through onWebResourceError (line 172-174). Users will see an infinite loading indicator if the page fails to load.

🔎 Suggested error state handling

Add an error state variable:

bool _isLoading = true;
String? _currentUrl;
String? _errorMessage;

Update onWebResourceError:

onWebResourceError: (WebResourceError error) {
  debugPrint('WebView Error: ${error.description}');
  if (mounted) {
    setState(() {
      _errorMessage = error.description;
      _isLoading = false;
    });
  }
},

Update the build method to show error state:

if (_errorMessage != null)
  Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Icon(Icons.error_outline, size: 48),
        const SizedBox(height: 16),
        Text('Failed to load page'),
        const SizedBox(height: 8),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _errorMessage = null;
              _isLoading = true;
            });
            _webViewController.reload();
          },
          child: const Text('Retry'),
        ),
      ],
    ),
  )
else if (model.pageData != null && !_isLoading)
  WebViewWidget(controller: _webViewController),
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 597f9fc and cad38ce.

📒 Files selected for processing (1)
  • lib/ui/views/ib/ib_page_view.dart
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-12T14:34:41.433Z
Learnt from: JatsuAkaYashvant
Repo: CircuitVerse/mobile-app PR: 421
File: lib/viewmodels/simulator/simulator_viewmodel.dart:244-244
Timestamp: 2025-08-12T14:34:41.433Z
Learning: In Flutter projects, `Uint8List` is re-exported by `flutter/services.dart`, so importing `dart:typed_data` directly is redundant when `flutter/services.dart` is already imported.

Applied to files:

  • lib/ui/views/ib/ib_page_view.dart
📚 Learning: 2025-08-12T14:34:41.433Z
Learnt from: JatsuAkaYashvant
Repo: CircuitVerse/mobile-app PR: 421
File: lib/viewmodels/simulator/simulator_viewmodel.dart:244-244
Timestamp: 2025-08-12T14:34:41.433Z
Learning: In Flutter projects, `Uint8List` is re-exported by `flutter/services.dart`, so importing `dart:typed_data` directly is redundant when `flutter/services.dart` is already imported. The Dart analyzer will confirm such imports as unnecessary.

Applied to files:

  • lib/ui/views/ib/ib_page_view.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test & Analyze (windows-latest)
  • GitHub Check: Test & Analyze (ubuntu-latest)
  • GitHub Check: Test & Analyze (macos-latest)
🔇 Additional comments (5)
lib/ui/views/ib/ib_page_view.dart (5)

1-1: LGTM!

The new imports are appropriate for the WebView-based implementation. dart:async is needed for the Timer functionality, and webview_flutter is the core dependency for rendering IB content in a WebView.

Also applies to: 15-15


52-55: LGTM!

State variable declarations are appropriate. The late final modifier for _webViewController is correct since it's initialized once in initState. Loading state and URL tracking variables properly support the WebView lifecycle.


198-208: LGTM!

The auto-hide timer implementation is clean and correct. The timer is properly cancelled before creating a new one, preventing timer leaks. The 5-second duration provides good UX for the floating navigation buttons.


66-66: URL validation is in place for unrestricted JavaScript mode.

The JavaScriptMode.unrestricted setting is acceptable here because the onNavigationRequest handler (lines 175-180) restricts WebView content to EnvironmentConfig.IB_BASE_URL (https://learn.circuitverse.org by default). External URLs are opened in the system browser instead, preventing malicious script injection through user navigation.


192-196: No action required. WebViewController in webview_flutter 4.13.0 does not provide a dispose() or close() method. Resource cleanup is handled automatically by the WebViewWidget's lifecycle and garbage collection. While webview_flutter 4.13.0 has known issues on GitHub related to lifecycle and hot-restart behavior, these are not addressed by manual controller disposal (which the API does not support). If stability is a concern, consider upgrading to a newer version of webview_flutter where these issues may have been resolved.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
lib/ui/views/ib/ib_page_view.dart (1)

78-87: _cleanPageJs constant is still duplicated inline in onPageStarted.

The cleanup function defined in _cleanPageJs (lines 78-87) is duplicated verbatim in the inline script at lines 125-133. This partially addresses the earlier DRY concern but the duplication remains.

🔎 Proposed fix to reuse the constant

Replace the inline duplicate in onPageStarted with the constant:

await _webViewController.runJavaScript('''
(function () {
  // ---- Cleanup old observer if any ----
  if (window.__ibMutationObserver) {
    try {
      window.__ibMutationObserver.disconnect();
    } catch (e) {}
    window.__ibMutationObserver = null;
  }

  $_cleanPageJs

  // Initial cleanup passes
  cleanPage();
  setTimeout(cleanPage, 50);
  setTimeout(cleanPage, 200);
  setTimeout(cleanPage, 500);

  // ---- Create observer ----
  const observer = new MutationObserver(() => {
    cleanPage();
  });

  observer.observe(document.body, {
    childList: true,
    subtree: true,
  });

  window.__ibMutationObserver = observer;

  // ---- Auto-disconnect after stabilization ----
  setTimeout(() => {
    if (window.__ibMutationObserver) {
      window.__ibMutationObserver.disconnect();
      window.__ibMutationObserver = null;
    }
  }, 2000);
})();
''');
🧹 Nitpick comments (1)
lib/ui/views/ib/ib_page_view.dart (1)

233-233: Prefer explicit Future<void> return type.

The method returns Future without a type parameter. Using Future<void> is more explicit and follows Dart best practices.

-Future _scrollToWidget(String slug) async {
+Future<void> _scrollToWidget(String slug) async {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cad38ce and be2bbd9.

📒 Files selected for processing (1)
  • lib/ui/views/ib/ib_page_view.dart
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-12T14:34:41.433Z
Learnt from: JatsuAkaYashvant
Repo: CircuitVerse/mobile-app PR: 421
File: lib/viewmodels/simulator/simulator_viewmodel.dart:244-244
Timestamp: 2025-08-12T14:34:41.433Z
Learning: In Flutter projects, `Uint8List` is re-exported by `flutter/services.dart`, so importing `dart:typed_data` directly is redundant when `flutter/services.dart` is already imported.

Applied to files:

  • lib/ui/views/ib/ib_page_view.dart
📚 Learning: 2025-08-12T14:34:41.433Z
Learnt from: JatsuAkaYashvant
Repo: CircuitVerse/mobile-app PR: 421
File: lib/viewmodels/simulator/simulator_viewmodel.dart:244-244
Timestamp: 2025-08-12T14:34:41.433Z
Learning: In Flutter projects, `Uint8List` is re-exported by `flutter/services.dart`, so importing `dart:typed_data` directly is redundant when `flutter/services.dart` is already imported. The Dart analyzer will confirm such imports as unnecessary.

Applied to files:

  • lib/ui/views/ib/ib_page_view.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test & Analyze (windows-latest)
  • GitHub Check: Test & Analyze (ubuntu-latest)
  • GitHub Check: Test & Analyze (macos-latest)
🔇 Additional comments (5)
lib/ui/views/ib/ib_page_view.dart (5)

1-46: LGTM!

The imports are appropriate for the WebView integration, and the enableWebView flag provides a useful toggle for testing or fallback scenarios.


115-161: MutationObserver lifecycle is now properly managed.

The implementation correctly disconnects any existing observer before creating a new one (lines 117-123) and auto-disconnects after 2 seconds of stabilization (lines 153-159). This addresses the previous memory leak concern.


213-223: LGTM!

Timer-based FAB visibility with proper cancellation in dispose is well implemented.


343-439: LGTM!

The floating action button implementation with animated visibility and Showcase integration is clean. The ChangeNotifierProvider pattern for state management is appropriate.


444-459: URL loading correctly moved out of build method.

The listener-based approach in onModelReady addresses the previous concern about side effects in the build method. This ensures loadRequest is only called when pageData actually changes.

@ShashwatXD ShashwatXD closed this Dec 31, 2025
@ShashwatXD ShashwatXD reopened this Dec 31, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
lib/ui/views/ib/ib_page_view.dart (1)

114-160: JavaScript cleanup logic is still partially duplicated.

The cleanPage function in onPageStarted (lines 124-132) duplicates the logic in _cleanPageJs constant (lines 79-86). Consider extracting the full injection script to a helper method for both callbacks.

🔎 Proposed refactor to consolidate scripts
String get _fullCleanupScript => '''
(function() {
  // Cleanup old observer if any
  if (window.__ibMutationObserver) {
    try { window.__ibMutationObserver.disconnect(); } catch (e) {}
    window.__ibMutationObserver = null;
  }

  $_cleanPageJs

  // Initial cleanup passes
  cleanPage();
  setTimeout(cleanPage, 50);
  setTimeout(cleanPage, 200);
  setTimeout(cleanPage, 500);

  // Create observer
  const observer = new MutationObserver(() => cleanPage());
  observer.observe(document.body, { childList: true, subtree: true });
  window.__ibMutationObserver = observer;

  // Auto-disconnect after stabilization
  setTimeout(() => {
    if (window.__ibMutationObserver) {
      window.__ibMutationObserver.disconnect();
      window.__ibMutationObserver = null;
    }
  }, 2000);
})();
''';

Then use _fullCleanupScript in both onPageStarted and onPageFinished.

Also applies to: 165-178

🧹 Nitpick comments (3)
lib/ui/views/ib/ib_page_view.dart (3)

94-94: Avoid using context in initState.

ShowCaseWidget.of(context) is called here in initState, but the widget tree may not be fully built yet. While this is also set in didChangeDependencies (line 207), the call here could be removed to avoid potential issues.

🔎 Proposed fix
  @override
  void initState() {
    _ibFloatingButtonState = IbFloatingButtonState();
    super.initState();
    _startHideTimer();
-   _showCaseWidgetState = ShowCaseWidget.of(context);

    _webViewController =

520-520: Hardcoded text should use localization.

The "Retry" button text is hardcoded in English. For consistency with the rest of the app, use AppLocalizations.

🔎 Proposed fix
                        ElevatedButton(
                          onPressed: () {
                            setState(() {
                              _webViewError = null;
                              _isLoading = true;
                            });
                            if (_currentUrl != null) {
                              _webViewController.loadRequest(
                                Uri.parse(_currentUrl!),
                              );
                            } else {
                              _webViewController.reload();
                            }
                          },
-                         child: const Text('Retry'),
+                         child: Text(AppLocalizations.of(context)!.retry),
                        ),

You may need to add the retry key to your localization files if it doesn't exist.


506-518: Retry logic has a potential edge case.

When _currentUrl is null, calling _webViewController.reload() may not work as expected if no URL was ever loaded. Consider falling back to the model's page URL instead.

🔎 Proposed fix
                          onPressed: () {
                            setState(() {
                              _webViewError = null;
                              _isLoading = true;
                            });
-                           // If current url is available, load it, else reload
-                           if (_currentUrl != null) {
-                             _webViewController.loadRequest(
-                               Uri.parse(_currentUrl!),
-                             );
-                           } else {
-                             _webViewController.reload();
-                           }
+                           final urlToLoad = _currentUrl ?? model.pageData?.pageUrl;
+                           if (urlToLoad != null) {
+                             _webViewController.loadRequest(Uri.parse(urlToLoad));
+                           }
                          },
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be2bbd9 and b815df3.

📒 Files selected for processing (1)
  • lib/ui/views/ib/ib_page_view.dart
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-12T14:34:41.433Z
Learnt from: JatsuAkaYashvant
Repo: CircuitVerse/mobile-app PR: 421
File: lib/viewmodels/simulator/simulator_viewmodel.dart:244-244
Timestamp: 2025-08-12T14:34:41.433Z
Learning: In Flutter projects, `Uint8List` is re-exported by `flutter/services.dart`, so importing `dart:typed_data` directly is redundant when `flutter/services.dart` is already imported.

Applied to files:

  • lib/ui/views/ib/ib_page_view.dart
📚 Learning: 2025-08-12T14:34:41.433Z
Learnt from: JatsuAkaYashvant
Repo: CircuitVerse/mobile-app PR: 421
File: lib/viewmodels/simulator/simulator_viewmodel.dart:244-244
Timestamp: 2025-08-12T14:34:41.433Z
Learning: In Flutter projects, `Uint8List` is re-exported by `flutter/services.dart`, so importing `dart:typed_data` directly is redundant when `flutter/services.dart` is already imported. The Dart analyzer will confirm such imports as unnecessary.

Applied to files:

  • lib/ui/views/ib/ib_page_view.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test & Analyze (ubuntu-latest)
  • GitHub Check: Test & Analyze (windows-latest)
  • GitHub Check: Test & Analyze (macos-latest)
🔇 Additional comments (6)
lib/ui/views/ib/ib_page_view.dart (6)

1-17: Imports are appropriate for the WebView-based implementation.

The addition of dart:async for Timer and webview_flutter for the WebView widget is correct for the new rendering approach.


46-57: State variables are well-structured for the WebView lifecycle.

Good use of nullable VoidCallback? for _modelListener to enable proper cleanup in dispose. The separation of loading, error, and URL state is clean.


211-219: Proper cleanup of listener and timer.

The listener is correctly removed from the model before disposal, and the timer is canceled. This addresses the previous memory leak concern.


452-467: Model listener setup is correctly implemented.

The listener is properly stored in _modelListener for cleanup in dispose, and the pattern for triggering page loads on model changes is well-structured.


351-447: Floating action buttons implementation is well-structured.

Good use of ChangeNotifierProvider.value with Consumer for reactive visibility, and disabling onPressed when invisible prevents accidental taps during the fade animation.


241-251: No security vulnerability here. The slug parameter is already sanitized by IbEngineService.getSlug() (lib/services/ib_engine_service.dart), which removes all special characters via replaceAll(RegExp(r'[^\w\s-]+'), ''). The result contains only alphanumerics, hyphens, and underscores—characters safe for JavaScript interpolation. The proposed escaping is unnecessary.

Likely an incorrect or invalid review comment.

@ShashwatXD
Copy link
Contributor Author

@JatsuAkaYashvant , please review , the ci is failing because the flutter_webview cant be opened in tests, thats a flutter limitation.

@tachyons
Copy link
Member

tachyons commented Jan 1, 2026

@ShashwatXD CI is critical and we can't merge this without fixing it. Is there any alternative gem which supports markdown rendering which is still maintained ?

@ShashwatXD
Copy link
Contributor Author

@ShashwatXD CI is critical and we can't merge this without fixing it. Is there any alternative gem which supports markdown rendering which is still maintained ?

I did try finding a few but i thought this can make it a lot future proof, because we have a interactive webview in a few chapters, so this was the best possible, rest i will try finding a better solution and let you know.

Also i have seen the simulator Page is also a web view page in this repo, that too doesnt have a test case, so i thought this could be acceptable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Debugging Interactive Book

2 participants