-
Notifications
You must be signed in to change notification settings - Fork 0
Implement Big Picture Mode with animations, E2E tests, and experimental EmulatorJS support #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b4c8763
077917a
b489554
2ee14a6
33233bf
3e7f08d
83b5795
a692ecd
30d578b
a5f11b5
7baf323
c4480a9
a84d7a3
c95882d
47a7eab
9a6feca
5346d27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { contextBridge, ipcRenderer } from "electron"; | ||
|
|
||
| contextBridge.exposeInMainWorld("crocdesk", { | ||
| revealInFolder: (filePath: string) => ipcRenderer.invoke("reveal-in-folder", filePath) | ||
| revealInFolder: (filePath: string) => ipcRenderer.invoke("reveal-in-folder", filePath), | ||
| toggleBigPicture: (enabled: boolean) => ipcRenderer.invoke("toggle-big-picture", enabled) | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,194 @@ | ||||||||||||||||||||
| import { useEffect, useRef, useState } from "react"; | ||||||||||||||||||||
| import "./emulator.css"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * EmulatorJS Component - Experimental | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * This component provides a web-based emulator interface for playing ROMs. | ||||||||||||||||||||
| * It's designed to work within Big Picture Mode for a console-like experience. | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * Requirements: | ||||||||||||||||||||
| * 1. EmulatorJS library must be served from /emulatorjs/ path | ||||||||||||||||||||
| * 2. ROM files must be accessible via URL | ||||||||||||||||||||
| * 3. BIOS files must be provided for certain systems (PS1, PSP, etc.) | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * Supported Systems: | ||||||||||||||||||||
| * - NES, SNES, GB/GBC/GBA, N64, PS1, PSP, DS, Arcade (MAME), Sega systems | ||||||||||||||||||||
| */ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export type EmulatorConfig = { | ||||||||||||||||||||
| core: string; // e.g., "nes", "snes", "gba", "n64", "psx", "psp" | ||||||||||||||||||||
| romUrl: string; | ||||||||||||||||||||
| biosUrl?: string; | ||||||||||||||||||||
| saveStateUrl?: string; | ||||||||||||||||||||
| gameId?: string; | ||||||||||||||||||||
| gameName?: string; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| type Props = { | ||||||||||||||||||||
| config: EmulatorConfig; | ||||||||||||||||||||
| onExit?: () => void; | ||||||||||||||||||||
| onError?: (error: Error) => void; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Platform to core mapping | ||||||||||||||||||||
| const CORE_MAP: Record<string, string> = { | ||||||||||||||||||||
| nes: "nes", | ||||||||||||||||||||
| snes: "snes", | ||||||||||||||||||||
| "game-boy": "gb", | ||||||||||||||||||||
| "game-boy-color": "gbc", | ||||||||||||||||||||
| "game-boy-advance": "gba", | ||||||||||||||||||||
| n64: "n64", | ||||||||||||||||||||
| ps1: "psx", | ||||||||||||||||||||
| psp: "psp", | ||||||||||||||||||||
| nds: "nds", | ||||||||||||||||||||
| genesis: "segaMD", | ||||||||||||||||||||
| "master-system": "segaMS", | ||||||||||||||||||||
| "game-gear": "segaGG", | ||||||||||||||||||||
| arcade: "mame" | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function getEmulatorCore(platform: string): string | null { | ||||||||||||||||||||
| return CORE_MAP[platform.toLowerCase()] || null; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export default function EmulatorPlayer({ config, onExit, onError }: Props) { | ||||||||||||||||||||
| const containerRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||
| const [isReady, setIsReady] = useState(false); | ||||||||||||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||
| // Check if EmulatorJS is available | ||||||||||||||||||||
| if (typeof (window as any).EJS_player === "undefined") { | ||||||||||||||||||||
| const errorMsg = "EmulatorJS library not found. Please ensure EmulatorJS is properly installed and served."; | ||||||||||||||||||||
| // Use setTimeout to avoid synchronous setState in effect | ||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||
| setError(errorMsg); | ||||||||||||||||||||
| onError?.(new Error(errorMsg)); | ||||||||||||||||||||
| }, 0); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!containerRef.current) return; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| try { | ||||||||||||||||||||
| // Initialize EmulatorJS | ||||||||||||||||||||
| const EJS = (window as any).EJS_player; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Configure EmulatorJS | ||||||||||||||||||||
| (window as any).EJS_core = config.core; | ||||||||||||||||||||
| (window as any).EJS_gameUrl = config.romUrl; | ||||||||||||||||||||
| (window as any).EJS_gameName = config.gameName || "Game"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (config.biosUrl) { | ||||||||||||||||||||
| (window as any).EJS_biosUrl = config.biosUrl; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (config.saveStateUrl) { | ||||||||||||||||||||
| (window as any).EJS_saveStateURL = config.saveStateUrl; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Set paths (assumes EmulatorJS is served from /emulatorjs/) | ||||||||||||||||||||
| (window as any).EJS_pathtodata = "/emulatorjs/data/"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Enable fullscreen by default | ||||||||||||||||||||
| (window as any).EJS_startFullscreen = false; // We handle fullscreen in Big Picture | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Disable ads | ||||||||||||||||||||
| (window as any).EJS_ads = false; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Initialize the player | ||||||||||||||||||||
| EJS("#emulator-container"); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| setIsReady(true); | ||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||
| const errorMsg = `Failed to initialize emulator: ${err}`; | ||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||
| setError(errorMsg); | ||||||||||||||||||||
| onError?.(new Error(errorMsg)); | ||||||||||||||||||||
| }, 0); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Cleanup | ||||||||||||||||||||
| return () => { | ||||||||||||||||||||
| // EmulatorJS cleanup if needed | ||||||||||||||||||||
| if ((window as any).EJS_emulator) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| (window as any).EJS_emulator.pause(); | ||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||
| console.error("Error pausing emulator:", e); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| }, [config, onError]); | ||||||||||||||||||||
|
||||||||||||||||||||
| }, [config, onError]); | |
| }, [ | |
| config.core, | |
| config.romUrl, | |
| config.biosUrl, | |
| config.saveStateUrl, | |
| config.gameName, | |
| onError | |
| ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The BigPicturePage component is wrapped in a BrowserRouter when bigPictureMode is true, but BigPicturePage uses router hooks (useNavigate, useLocation) which expect to be within a router context. This creates a nested router structure where the BigPicturePage route is never actually matched, since it's not within the Routes definition. The component will work for navigation but bypasses the routing system.
Consider either: