import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../injection/script_engine.dart'; import '../injection/script_registry.dart'; import '../channels/channel_registry.dart'; import '../webview/webview_config.dart'; import '../services/ghost_mode_service.dart'; class InstagramWebView extends StatefulWidget { const InstagramWebView({super.key}); @override State createState() => InstagramWebViewState(); } class InstagramWebViewState extends State { InAppWebViewController? _controller; ScriptEngine? _engine; GhostModeService? _ghostMode; bool _loading = true; // ── Public API — call from Settings screen ───────────────────────────── Future toggleScript(ScriptId id, bool enabled) async { await _engine?.toggle(id, enabled); } Future setContentFlag(String flag, bool value) async { await _engine?.setContentFlag(flag, value); } Future setOnlineHide(bool enabled) async { await _engine?.setOnlineHide(enabled); } // Ghost mode controls Future setGhostModeEnabled(bool enabled) async { if (_ghostMode != null) { _ghostMode!.features.disableAnalytics = enabled; _ghostMode!.features.hideStoryViews = enabled; _ghostMode!.features.hideReadReceipts = enabled; _ghostMode!.features.hideLiveJoin = enabled; _ghostMode!.features.hideTypingIndicator = enabled; _ghostMode!.features.hideVoiceListened = enabled; _ghostMode!.features.hideReplyImageViewed = enabled; await _ghostMode!.features.save(); // Reapply settings if webview exists if (_controller != null) { // Force reload to apply new settings await _controller!.reload(); } } } Future setAntiScreenshot(bool enabled) async { if (_ghostMode != null) { _ghostMode!.features.antiScreenshot = enabled; await _ghostMode!.features.save(); if (_ghostMode!.features.antiScreenshot) { await _ghostMode!.applyWindowFlags(context); } else { await _ghostMode!.clearWindowFlags(); } } } // ───────────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { return Stack( children: [ InAppWebView( initialUrlRequest: WebViewConfig.initialRequest, initialSettings: _ghostMode?.buildWebViewSettings() ?? WebViewConfig.settings, // ── ContentBlockers — merged base + EasyList rules ────────────── contentBlockers: WebViewConfig.baseContentBlockers, // TODO Phase 1.5: merge EasyListParser.load() here at startup // ── User Scripts — AT_DOCUMENT_START critical for ghost mode ───── initialUserScripts: UnmodifiableListView( _ghostMode?.buildUserScripts() ?? [], ), // ── JavaScript channels ───────────────────────────────────────── javascriptChannels: ChannelRegistry( onActivityEvent: (event) { // Forward to history DB in Phase 2 debugPrint('[Activity] $event'); }, ).build(), onWebViewCreated: (controller) async { _controller = controller; // Initialize GhostModeService _ghostMode = GhostModeService(); await _ghostMode!.load(); // Initialize existing script engine for other scripts final prefs = await SharedPreferences.getInstance(); _engine = ScriptEngine(controller: controller, prefs: prefs); // Inject DOCUMENT_START scripts (ghost mode, etc.) await _engine!.initDocumentStartScripts(); }, onLoadStop: (controller, url) async { // Inject DOCUMENT_END scripts await _engine?.injectDocumentEndScripts(); // Re-inject ghost mode scripts on SPA navigation await _ghostMode?.onPageLoaded(url?.uriValue); setState(() => _loading = false); }, onLoadStart: (controller, url) { setState(() => _loading = true); }, onProgressChanged: (controller, progress) { if (progress >= 80 && _loading) { setState(() => _loading = false); } }, // ── Navigation policy ─────────────────────────────────────────── shouldOverrideUrlLoading: (controller, navigationAction) async { final url = navigationAction.request.url?.toString() ?? ''; // Block external redirects — keep user inside instagram.com if (!url.contains('instagram.com') && !url.contains('cdninstagram.com') && !url.contains('fbcdn.net') && url.startsWith('http')) { // TODO: open in external browser via url_launcher return NavigationActionPolicy.CANCEL; } return NavigationActionPolicy.ALLOW; }, // ── Re-inject on SPA navigation ───────────────────────────────── // Instagram is a SPA — URL changes via pushState don't trigger // onLoadStop. Re-inject DOM scripts on URL change. onUpdateVisitedHistory: (controller, url, isReload) async { if (!isReload!) { await _engine?.injectDocumentEndScripts(); } }, // ── Native intercept for service worker requests ──────────────── shouldInterceptRequest: (controller, request) async { return await _ghostMode?.shouldInterceptRequest( controller, request, ) ?? null; }, ), // ── Subtle loading indicator ────────────────────────────────────── if (_loading) const LinearProgressIndicator( minHeight: 2, backgroundColor: Colors.transparent, ), ], ); } }