preload flutter
This commit is contained in:
@@ -28,60 +28,52 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
type FlutterViewProps = {
|
||||
state: FlutterTimelineState;
|
||||
onEvent: (event: FlutterEvent) => void;
|
||||
className?: string;
|
||||
height?: number;
|
||||
};
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module-level singleton state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function FlutterView({
|
||||
state,
|
||||
onEvent,
|
||||
className,
|
||||
height,
|
||||
}: FlutterViewProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [status, setStatus] = useState<"loading" | "ready" | "error">(
|
||||
"loading"
|
||||
);
|
||||
const stateRef = useRef(state);
|
||||
stateRef.current = state;
|
||||
let flutterContainer: HTMLDivElement | null = null;
|
||||
let engineReady = false;
|
||||
let initPromise: Promise<void> | null = null;
|
||||
|
||||
const onEventRef = useRef(onEvent);
|
||||
onEventRef.current = onEvent;
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
let pollingInterval: ReturnType<typeof setInterval> | undefined;
|
||||
let pollingTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
// 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(stateRef.current),
|
||||
getState: () => JSON.stringify(currentStateRef),
|
||||
onEvent: (json: string) => {
|
||||
const event = JSON.parse(json) as FlutterEvent;
|
||||
onEventRef.current(event);
|
||||
currentOnEventRef?.(event);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
export function preloadFlutter() {
|
||||
initFlutter();
|
||||
}
|
||||
|
||||
function initFlutter(): Promise<void> {
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
initPromise = (async () => {
|
||||
ensureBridge();
|
||||
// Load flutter.js if not already loaded
|
||||
if (!window._flutter) {
|
||||
await new Promise<void>((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);
|
||||
const timeout = setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
reject(
|
||||
new Error("Timed out waiting for flutter.js to initialize")
|
||||
new Error("Timed out waiting for flutter.js to initialize"),
|
||||
);
|
||||
}, 10_000);
|
||||
pollingInterval = setInterval(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (window._flutter) {
|
||||
clearInterval(pollingInterval);
|
||||
clearTimeout(pollingTimeout);
|
||||
clearInterval(interval);
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
}, 50);
|
||||
@@ -105,7 +97,13 @@ export function FlutterView({
|
||||
window._flutter!.buildConfig = await res.json();
|
||||
}
|
||||
|
||||
try {
|
||||
// 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/",
|
||||
@@ -113,27 +111,81 @@ export function FlutterView({
|
||||
},
|
||||
onEntrypointLoaded: async (engineInitializer) => {
|
||||
const appRunner = await engineInitializer.initializeEngine({
|
||||
hostElement: container,
|
||||
hostElement: flutterContainer!,
|
||||
assetBase: "/flutter/",
|
||||
});
|
||||
appRunner.runApp();
|
||||
setStatus("ready");
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Flutter engine initialization failed: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
|
||||
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;
|
||||
className?: string;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
init().catch(() => setStatus("error"));
|
||||
export function FlutterView({
|
||||
state,
|
||||
onEvent,
|
||||
className,
|
||||
height,
|
||||
}: FlutterViewProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [status, setStatus] = useState<"loading" | "ready" | "error">(
|
||||
engineReady ? "ready" : "loading",
|
||||
);
|
||||
|
||||
// Keep module-level refs in sync
|
||||
currentStateRef = state;
|
||||
currentOnEventRef = onEvent;
|
||||
|
||||
useEffect(() => {
|
||||
const wrapper = containerRef.current;
|
||||
if (!wrapper) return;
|
||||
|
||||
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<Window>).__zendegi__;
|
||||
// Detach but don't destroy the Flutter container
|
||||
if (flutterContainer?.parentElement === wrapper) {
|
||||
wrapper.removeChild(flutterContainer);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<section className="container mx-auto max-w-3xl px-4 py-2">
|
||||
<h1 className="text-4xl font-serif font-bold mb-6">Timelines</h1>
|
||||
|
||||
Reference in New Issue
Block a user