preload flutter
This commit is contained in:
@@ -28,60 +28,52 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlutterViewProps = {
|
// ---------------------------------------------------------------------------
|
||||||
state: FlutterTimelineState;
|
// Module-level singleton state
|
||||||
onEvent: (event: FlutterEvent) => void;
|
// ---------------------------------------------------------------------------
|
||||||
className?: string;
|
|
||||||
height?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FlutterView({
|
let flutterContainer: HTMLDivElement | null = null;
|
||||||
state,
|
let engineReady = false;
|
||||||
onEvent,
|
let initPromise: Promise<void> | null = null;
|
||||||
className,
|
|
||||||
height,
|
|
||||||
}: FlutterViewProps) {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [status, setStatus] = useState<"loading" | "ready" | "error">(
|
|
||||||
"loading"
|
|
||||||
);
|
|
||||||
const stateRef = useRef(state);
|
|
||||||
stateRef.current = state;
|
|
||||||
|
|
||||||
const onEventRef = useRef(onEvent);
|
// Module-level refs so bridge closures always read latest values
|
||||||
onEventRef.current = onEvent;
|
let currentStateRef: FlutterTimelineState | null = null;
|
||||||
|
let currentOnEventRef: ((event: FlutterEvent) => void) | null = null;
|
||||||
useEffect(() => {
|
|
||||||
const container = containerRef.current;
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
let pollingInterval: ReturnType<typeof setInterval> | undefined;
|
|
||||||
let pollingTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
|
|
||||||
|
function ensureBridge() {
|
||||||
|
if (window.__zendegi__) return;
|
||||||
window.__zendegi__ = {
|
window.__zendegi__ = {
|
||||||
getState: () => JSON.stringify(stateRef.current),
|
getState: () => JSON.stringify(currentStateRef),
|
||||||
onEvent: (json: string) => {
|
onEvent: (json: string) => {
|
||||||
const event = JSON.parse(json) as FlutterEvent;
|
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
|
// Load flutter.js if not already loaded
|
||||||
if (!window._flutter) {
|
if (!window._flutter) {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
if (document.querySelector('script[src="/flutter/flutter.js"]')) {
|
if (document.querySelector('script[src="/flutter/flutter.js"]')) {
|
||||||
// Script tag exists but hasn't finished — wait for _flutter to appear
|
const timeout = setTimeout(() => {
|
||||||
pollingTimeout = setTimeout(() => {
|
clearInterval(interval);
|
||||||
clearInterval(pollingInterval);
|
|
||||||
reject(
|
reject(
|
||||||
new Error("Timed out waiting for flutter.js to initialize")
|
new Error("Timed out waiting for flutter.js to initialize"),
|
||||||
);
|
);
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
pollingInterval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (window._flutter) {
|
if (window._flutter) {
|
||||||
clearInterval(pollingInterval);
|
clearInterval(interval);
|
||||||
clearTimeout(pollingTimeout);
|
clearTimeout(timeout);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
@@ -105,7 +97,13 @@ export function FlutterView({
|
|||||||
window._flutter!.buildConfig = await res.json();
|
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({
|
await window._flutter!.loader.load({
|
||||||
config: {
|
config: {
|
||||||
entrypointBaseUrl: "/flutter/",
|
entrypointBaseUrl: "/flutter/",
|
||||||
@@ -113,27 +111,81 @@ export function FlutterView({
|
|||||||
},
|
},
|
||||||
onEntrypointLoaded: async (engineInitializer) => {
|
onEntrypointLoaded: async (engineInitializer) => {
|
||||||
const appRunner = await engineInitializer.initializeEngine({
|
const appRunner = await engineInitializer.initializeEngine({
|
||||||
hostElement: container,
|
hostElement: flutterContainer!,
|
||||||
assetBase: "/flutter/",
|
assetBase: "/flutter/",
|
||||||
});
|
});
|
||||||
appRunner.runApp();
|
appRunner.runApp();
|
||||||
setStatus("ready");
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
throw new Error(
|
engineReady = true;
|
||||||
`Flutter engine initialization failed: ${error instanceof Error ? error.message : error}`
|
})().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 () => {
|
return () => {
|
||||||
clearInterval(pollingInterval);
|
// Detach but don't destroy the Flutter container
|
||||||
clearTimeout(pollingTimeout);
|
if (flutterContainer?.parentElement === wrapper) {
|
||||||
container.replaceChildren();
|
wrapper.removeChild(flutterContainer);
|
||||||
delete (window as Partial<Window>).__zendegi__;
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { timelinesQueryOptions } from "@/functions/get-timelines";
|
import { timelinesQueryOptions } from "@/functions/get-timelines";
|
||||||
|
import { preloadFlutter } from "@/components/flutter-view";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_default/timelines")({
|
export const Route = createFileRoute("/_default/timelines")({
|
||||||
loader: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
@@ -11,6 +13,10 @@ export const Route = createFileRoute("/_default/timelines")({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const timelinesQuery = useSuspenseQuery(timelinesQueryOptions());
|
const timelinesQuery = useSuspenseQuery(timelinesQueryOptions());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
preloadFlutter();
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<section className="container mx-auto max-w-3xl px-4 py-2">
|
<section className="container mx-auto max-w-3xl px-4 py-2">
|
||||||
<h1 className="text-4xl font-serif font-bold mb-6">Timelines</h1>
|
<h1 className="text-4xl font-serif font-bold mb-6">Timelines</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user