130 lines
3.3 KiB
TypeScript
130 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
export type JsDosPlayerProps = {
|
|
zipUrl?: string;
|
|
className?: string;
|
|
};
|
|
|
|
export type JsDosHandle = {
|
|
stop: () => void;
|
|
};
|
|
|
|
interface DosInstance {
|
|
stop(): void;
|
|
}
|
|
|
|
export function JsDosPlayer({ zipUrl, className = "" }: JsDosPlayerProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const dosInstanceRef = useRef<DosInstance | null>(null);
|
|
|
|
const ensureJsDosCss = () => {
|
|
if (typeof document === "undefined") return;
|
|
if (!document.querySelector('link[data-jsdos]')) {
|
|
const link = document.createElement("link");
|
|
link.rel = "stylesheet";
|
|
link.href = "https://v8.js-dos.com/v7/js-dos.css";
|
|
link.setAttribute("data-jsdos", "true");
|
|
document.head.appendChild(link);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current || !zipUrl) return;
|
|
|
|
let isMounted = true;
|
|
|
|
const loadEmulator = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
// Dynamically import js-dos at runtime (CSS injected via CDN)
|
|
ensureJsDosCss();
|
|
const jsDosModule = await import("js-dos");
|
|
const { Dos } = jsDosModule;
|
|
|
|
if (!isMounted || !containerRef.current) return;
|
|
|
|
// Create DOS instance
|
|
const dos = Dos(containerRef.current);
|
|
dosInstanceRef.current = dos;
|
|
|
|
// Run the provided archive
|
|
await dos.run(zipUrl);
|
|
|
|
if (isMounted) {
|
|
setIsLoading(false);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to load js-dos:", err);
|
|
if (isMounted) {
|
|
setError(err instanceof Error ? err.message : "Failed to load emulator");
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
loadEmulator();
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
// Cleanup DOS instance
|
|
if (dosInstanceRef.current) {
|
|
try {
|
|
dosInstanceRef.current.stop();
|
|
} catch (err) {
|
|
console.error("Error stopping DOS instance:", err);
|
|
}
|
|
dosInstanceRef.current = null;
|
|
}
|
|
};
|
|
}, [zipUrl]);
|
|
|
|
const handleStop = () => {
|
|
if (dosInstanceRef.current) {
|
|
try {
|
|
dosInstanceRef.current.stop();
|
|
dosInstanceRef.current = null;
|
|
} catch (err) {
|
|
console.error("Error stopping DOS instance:", err);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (!zipUrl) {
|
|
return (
|
|
<div className={`flex items-center justify-center p-8 text-neutral-400 ${className}`}>
|
|
<p>No archive URL provided</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className={`flex flex-col items-center justify-center p-8 text-neutral-400 ${className}`}>
|
|
<p className="text-red-400 mb-4">Error: {error}</p>
|
|
<p className="text-sm">Please check the archive URL or try a different file.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
{isLoading && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
|
|
<div className="text-neutral-200">Loading emulator...</div>
|
|
</div>
|
|
)}
|
|
<div
|
|
ref={containerRef}
|
|
className="w-full h-full min-h-[400px] bg-black rounded"
|
|
style={{ minHeight: "400px" }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|