From c39dcb61649583c4f2c0ecfe033d69f26276f72f Mon Sep 17 00:00:00 2001 From: Jonatan Granqvist Date: Sun, 8 Mar 2026 09:36:43 +0100 Subject: [PATCH] preload flutter --- apps/web/src/components/flutter-view.tsx | 220 +++++++++++++-------- apps/web/src/routes/_default.timelines.tsx | 6 + 2 files changed, 142 insertions(+), 84 deletions(-) diff --git a/apps/web/src/components/flutter-view.tsx b/apps/web/src/components/flutter-view.tsx index af425e6..b72e9b4 100644 --- a/apps/web/src/components/flutter-view.tsx +++ b/apps/web/src/components/flutter-view.tsx @@ -28,6 +28,112 @@ declare global { } } +// --------------------------------------------------------------------------- +// Module-level singleton state +// --------------------------------------------------------------------------- + +let flutterContainer: HTMLDivElement | null = null; +let engineReady = false; +let initPromise: Promise | null = null; + +// Module-level refs so bridge closures always read latest values +let currentStateRef: FlutterTimelineState | null = null; +let currentOnEventRef: ((event: FlutterEvent) => void) | null = null; + +function ensureBridge() { + if (window.__zendegi__) return; + window.__zendegi__ = { + getState: () => JSON.stringify(currentStateRef), + onEvent: (json: string) => { + const event = JSON.parse(json) as FlutterEvent; + currentOnEventRef?.(event); + }, + }; +} + +export function preloadFlutter() { + initFlutter(); +} + +function initFlutter(): Promise { + if (initPromise) return initPromise; + + initPromise = (async () => { + ensureBridge(); + // Load flutter.js if not already loaded + if (!window._flutter) { + await new Promise((resolve, reject) => { + if (document.querySelector('script[src="/flutter/flutter.js"]')) { + const timeout = setTimeout(() => { + clearInterval(interval); + reject( + new Error("Timed out waiting for flutter.js to initialize"), + ); + }, 10_000); + const interval = setInterval(() => { + if (window._flutter) { + clearInterval(interval); + clearTimeout(timeout); + resolve(); + } + }, 50); + return; + } + const script = document.createElement("script"); + script.src = "/flutter/flutter.js"; + script.defer = true; + script.onload = () => resolve(); + script.onerror = () => reject(new Error("Failed to load flutter.js")); + document.head.appendChild(script); + }); + } + + // Fetch and set buildConfig so load() works (supports wasm) + if (!window._flutter!.buildConfig) { + const res = await fetch("/flutter/build_config.json"); + if (!res.ok) { + throw new Error(`Failed to fetch build config: ${res.status}`); + } + window._flutter!.buildConfig = await res.json(); + } + + // Create the persistent container once + if (!flutterContainer) { + flutterContainer = document.createElement("div"); + flutterContainer.style.width = "100%"; + flutterContainer.style.height = "100%"; + } + + await window._flutter!.loader.load({ + config: { + entrypointBaseUrl: "/flutter/", + assetBase: "/flutter/", + }, + onEntrypointLoaded: async (engineInitializer) => { + const appRunner = await engineInitializer.initializeEngine({ + hostElement: flutterContainer!, + assetBase: "/flutter/", + }); + appRunner.runApp(); + }, + }); + + engineReady = true; + })().catch((error) => { + // Reset all module state so next mount retries + flutterContainer = null; + engineReady = false; + initPromise = null; + throw error; + }); + + return initPromise; +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + type FlutterViewProps = { state: FlutterTimelineState; onEvent: (event: FlutterEvent) => void; @@ -43,97 +149,43 @@ export function FlutterView({ }: FlutterViewProps) { const containerRef = useRef(null); const [status, setStatus] = useState<"loading" | "ready" | "error">( - "loading" + engineReady ? "ready" : "loading", ); - const stateRef = useRef(state); - stateRef.current = state; - const onEventRef = useRef(onEvent); - onEventRef.current = onEvent; + // Keep module-level refs in sync + currentStateRef = state; + currentOnEventRef = onEvent; useEffect(() => { - const container = containerRef.current; - if (!container) return; + const wrapper = containerRef.current; + if (!wrapper) return; - let pollingInterval: ReturnType | undefined; - let pollingTimeout: ReturnType | undefined; - - window.__zendegi__ = { - getState: () => JSON.stringify(stateRef.current), - onEvent: (json: string) => { - const event = JSON.parse(json) as FlutterEvent; - onEventRef.current(event); - }, - }; - - const init = async () => { - // Load flutter.js if not already loaded - if (!window._flutter) { - await new Promise((resolve, reject) => { - if (document.querySelector('script[src="/flutter/flutter.js"]')) { - // Script tag exists but hasn't finished — wait for _flutter to appear - pollingTimeout = setTimeout(() => { - clearInterval(pollingInterval); - reject( - new Error("Timed out waiting for flutter.js to initialize") - ); - }, 10_000); - pollingInterval = setInterval(() => { - if (window._flutter) { - clearInterval(pollingInterval); - clearTimeout(pollingTimeout); - resolve(); - } - }, 50); - return; - } - const script = document.createElement("script"); - script.src = "/flutter/flutter.js"; - script.defer = true; - script.onload = () => resolve(); - script.onerror = () => reject(new Error("Failed to load flutter.js")); - document.head.appendChild(script); - }); - } - - // Fetch and set buildConfig so load() works (supports wasm) - if (!window._flutter!.buildConfig) { - const res = await fetch("/flutter/build_config.json"); - if (!res.ok) { - throw new Error(`Failed to fetch build config: ${res.status}`); - } - window._flutter!.buildConfig = await res.json(); - } - - try { - await window._flutter!.loader.load({ - config: { - entrypointBaseUrl: "/flutter/", - assetBase: "/flutter/", - }, - onEntrypointLoaded: async (engineInitializer) => { - const appRunner = await engineInitializer.initializeEngine({ - hostElement: container, - assetBase: "/flutter/", - }); - appRunner.runApp(); - setStatus("ready"); - }, - }); - } catch (error) { - throw new Error( - `Flutter engine initialization failed: ${error instanceof Error ? error.message : error}` - ); - } - }; - - init().catch(() => setStatus("error")); + if (engineReady && flutterContainer) { + // Engine already running — reparent immediately, no loading flash + wrapper.appendChild(flutterContainer); + setStatus("ready"); + // Push latest state so Flutter renders the correct timeline + window.__zendegi__?.updateState?.(JSON.stringify(currentStateRef)); + } else { + // First mount — initialize engine + setStatus("loading"); + initFlutter() + .then(() => { + // Guard: component may have unmounted while awaiting + if (!containerRef.current || !flutterContainer) return; + wrapper.appendChild(flutterContainer); + setStatus("ready"); + // Push current state after engine is ready + window.__zendegi__?.updateState?.(JSON.stringify(currentStateRef)); + }) + .catch(() => setStatus("error")); + } return () => { - clearInterval(pollingInterval); - clearTimeout(pollingTimeout); - container.replaceChildren(); - delete (window as Partial).__zendegi__; + // Detach but don't destroy the Flutter container + if (flutterContainer?.parentElement === wrapper) { + wrapper.removeChild(flutterContainer); + } }; }, []); diff --git a/apps/web/src/routes/_default.timelines.tsx b/apps/web/src/routes/_default.timelines.tsx index cad0169..52b0748 100644 --- a/apps/web/src/routes/_default.timelines.tsx +++ b/apps/web/src/routes/_default.timelines.tsx @@ -1,6 +1,8 @@ +import { useEffect } from "react"; import { createFileRoute, Link } from "@tanstack/react-router"; import { useSuspenseQuery } from "@tanstack/react-query"; import { timelinesQueryOptions } from "@/functions/get-timelines"; +import { preloadFlutter } from "@/components/flutter-view"; export const Route = createFileRoute("/_default/timelines")({ loader: async ({ context }) => { @@ -11,6 +13,10 @@ export const Route = createFileRoute("/_default/timelines")({ function RouteComponent() { const timelinesQuery = useSuspenseQuery(timelinesQueryOptions()); + + useEffect(() => { + preloadFlutter(); + }, []); return (

Timelines