web: Rewrite demo in react

This commit is contained in:
Nathan Adams 2023-11-22 21:10:22 +01:00
parent fe99e6350b
commit 6f1cc89c47
30 changed files with 2070 additions and 720 deletions

1552
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@
"build:debug": "cross-env NODE_ENV=development CARGO_FEATURES=avm_debug npm run build",
"build:dual-wasm": "cross-env ENABLE_WASM_EXTENSIONS=true npm run build",
"build:repro": "cross-env ENABLE_WASM_EXTENSIONS=true ENABLE_CARGO_CLEAN=true ENABLE_VERSION_SEAL=true npm run build",
"demo": "npm start --workspace ruffle-demo",
"demo": "npm preview --workspace ruffle-demo",
"test": "npm test --workspaces --if-present",
"docs": "npm run docs --workspaces --if-present",
"lint": "eslint . && stylelint **.css",

View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View File

@ -1,4 +0,0 @@
env:
browser: true
parserOptions:
sourceType: module

View File

@ -1 +1 @@
/swfs.json
/public/swfs.json

View File

@ -17,7 +17,7 @@ After [building ruffle-web](https://github.com/ruffle-rs/ruffle/blob/master/web/
you can run `npm run demo` in the `web` folder to launch the demo.
It will start a local web server and print the address in the console.
Navigate to that website (usually [http://localhost:8080](http://localhost:8080)) in your browser.
Navigate to that website (usually [http://localhost:4173](http://localhost:4173)) in your browser.
### Configuring the demo

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/icon32.png" sizes="32x32">
<link rel="icon" href="/icon180.png" sizes="180x180">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ruffle Player</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -4,17 +4,28 @@
"description": "Demo of Ruffle Flash emulator",
"license": "(MIT OR Apache-2.0)",
"private": true,
"type": "module",
"scripts": {
"build": "webpack",
"start": "webpack serve"
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ruffle-core": "^0.1.0"
},
"devDependencies": {
"css-loader": "^6.8.1",
"style-loader": "^3.3.3",
"ts-loader": "^9.5.0",
"webpack-cli": "^5.1.4"
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.2.0",
"eslint": "^8.53.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,98 @@
import { Player } from "./player.tsx";
import { useRef, useState, DragEvent, useCallback } from "react";
import { BaseLoadOptions, MovieMetadata } from "ruffle-core";
import { Navbar } from "./navbar.tsx";
import { MetadataPanel } from "./metadata.tsx";
export function App(baseConfig: BaseLoadOptions) {
const [metadata, setMetadata] = useState<MovieMetadata | null>(null);
const [metadataVisible, setMetadataVisible] = useState<boolean>(false);
const [selectedFilename, setSelectedFilename] = useState<string | null>(
null,
);
const [dragOverlayVisible, setDragOverlayVisible] =
useState<boolean>(false);
const player = useRef<Player>(null);
const toggleMetadataVisible = () => {
setMetadataVisible(!metadataVisible);
};
const reloadMovie = () => {
player.current?.reload();
};
const onSelectUrl = useCallback((url: string, options: BaseLoadOptions) => {
player.current?.loadUrl(url, options);
}, []);
const onSelectFile = (file: File) => {
player.current?.loadFile(file);
};
const onFileDragEnter = (event: DragEvent<HTMLElement>) => {
event.stopPropagation();
event.preventDefault();
};
const onFileDragLeave = (event: DragEvent<HTMLElement>) => {
event.stopPropagation();
event.preventDefault();
setDragOverlayVisible(false);
};
const onFileDragOver = (event: DragEvent<HTMLElement>) => {
event.stopPropagation();
event.preventDefault();
setDragOverlayVisible(true);
};
const onFileDragDrop = (event: DragEvent<HTMLElement>) => {
event.stopPropagation();
event.preventDefault();
setDragOverlayVisible(false);
if (event.dataTransfer) {
setSelectedFilename(event.dataTransfer.files[0].name);
player.current?.loadFile(event.dataTransfer.files[0]);
}
};
return (
<>
<Navbar
onToggleMetadata={toggleMetadataVisible}
onReloadMovie={reloadMovie}
onSelectUrl={onSelectUrl}
onSelectFile={onSelectFile}
selectedFilename={selectedFilename}
setSelectedFilename={setSelectedFilename}
onFileDragLeave={onFileDragLeave}
onFileDragOver={onFileDragOver}
onFileDragDrop={onFileDragDrop}
/>
<div
id="main"
className={metadataVisible ? "info-container-shown" : ""}
>
<Player
id="player-container"
aria-label="Select a demo or drag an SWF"
onLoadedMetadata={setMetadata}
ref={player}
onDragEnter={onFileDragEnter}
onDragLeave={onFileDragLeave}
onDragOver={onFileDragOver}
onDragDrop={onFileDragDrop}
baseConfig={baseConfig}
>
<div
id="overlay"
className={dragOverlayVisible ? "drag" : ""}
></div>
</Player>
<MetadataPanel visible={metadataVisible} metadata={metadata} />
</div>
</>
);
}

View File

@ -1,4 +1,4 @@
body {
#root {
position: absolute;
inset: 0;
padding: 0;
@ -103,6 +103,10 @@ body {
transition-timing-function: ease-out;
}
.info-container-shown #player {
width: calc(100% - 300px);
}
#overlay:not([hidden]) ~ #player {
bottom: 100%;
}
@ -119,10 +123,6 @@ body {
box-sizing: border-box;
}
#player.info-container-shown {
width: calc(100% - 300px);
}
#info-container span:first-child {
text-shadow: 0 0 1px white;
}

View File

@ -1,339 +0,0 @@
import "./lato.css";
import "./common.css";
import "./index.css";
declare global {
interface Navigator {
/**
* iPadOS sends a User-Agent string that appears to be from macOS.
* navigator.standalone is not defined on macOS, so we use it for iPad detection.
*/
standalone?: boolean;
}
}
import {
BaseLoadOptions,
DataLoadOptions,
Letterbox,
LogLevel,
PublicAPI,
RufflePlayer,
URLLoadOptions,
} from "ruffle-core";
window.RufflePlayer = PublicAPI.negotiate(window.RufflePlayer, "local");
const ruffle = (window.RufflePlayer as PublicAPI).newest()!;
let player: RufflePlayer | null;
const playerContainer = document.getElementById("player-container")!;
const overlay = document.getElementById("overlay")!;
const authorContainer = document.getElementById("author-container")!;
const author = document.getElementById("author") as HTMLLinkElement;
const webUrlInputContainer = document.getElementById("web-url-container")!;
const sampleFileInputContainer = document.getElementById(
"sample-swfs-container",
)!;
const localFileInput = document.getElementById(
"local-file",
) as HTMLInputElement;
const sampleFileInput = document.getElementById(
"sample-swfs",
) as HTMLSelectElement;
const localFileName = document.getElementById("local-file-name")!;
const toggleInfo = document.getElementById("toggle-info")!;
const reloadSwf = document.getElementById("reload-swf")!;
const infoContainer = document.getElementById("info-container")!;
// prettier-ignore
const optionGroups = {
"Animation": document.getElementById("anim-optgroup")!,
"Game": document.getElementById("games-optgroup")!,
};
// This is the base config used by the demo player (except for specific SWF files
// with their own base config).
// It has the highest priority and its options cannot be overwritten.
const baseDemoConfig = {
letterbox: Letterbox.On,
logLevel: LogLevel.Warn,
forceScale: true,
forceAlign: true,
};
// [NA] For when we consolidate the extension player, and the web demo
const enableUrlInput = false;
if (!enableUrlInput) {
webUrlInputContainer.classList.add("hidden");
}
const swfToFlashVersion: { [key: number]: string } = {
1: "1",
2: "2",
3: "3",
4: "4",
5: "5",
6: "6",
7: "7",
8: "8",
9: "9.0",
10: "10.0/10.1",
11: "10.2",
12: "10.3",
13: "11.0",
14: "11.1",
15: "11.2",
16: "11.3",
17: "11.4",
18: "11.5",
19: "11.6",
20: "11.7",
21: "11.8",
22: "11.9",
23: "12",
24: "13",
25: "14",
26: "15",
27: "16",
28: "17",
29: "18",
30: "19",
31: "20",
32: "21",
33: "22",
34: "23",
35: "24",
36: "25",
37: "26",
38: "27",
39: "28",
40: "29",
41: "30",
42: "31",
43: "32",
};
interface DemoSwf {
location: string;
title?: string;
author?: string;
authorLink?: string;
config?: BaseLoadOptions;
type: "Animation" | "Game";
}
interface HTMLOptionElementWithSwf extends HTMLOptionElement {
swfData: DemoSwf;
}
function unload() {
if (player) {
player.remove();
document.querySelectorAll("span.metadata").forEach((el) => {
el.textContent = "Loading";
});
document.getElementById("backgroundColor")!.style.backgroundColor =
"white";
}
}
function load(options: string | DataLoadOptions | URLLoadOptions) {
unload();
player = ruffle.createPlayer();
player.id = "player";
playerContainer.append(player);
player.load(options, false);
player.addEventListener("loadedmetadata", () => {
if (player?.metadata) {
for (const [key, value] of Object.entries(player.metadata)) {
const metadataElement = document.getElementById(key);
if (metadataElement) {
switch (key) {
case "backgroundColor":
metadataElement.style.backgroundColor =
value ?? "white";
break;
case "uncompressedLength":
metadataElement.textContent = `${value >> 10}Kb`;
break;
// @ts-expect-error This intentionally falls through to the default case
case "swfVersion":
document.getElementById(
"flashVersion",
)!.textContent = swfToFlashVersion[value] ?? null;
// falls through and executes the default case as well
default:
metadataElement.textContent = value;
break;
}
}
}
}
});
}
function showSample(swfData: DemoSwf) {
authorContainer.classList.remove("hidden");
author.textContent = swfData.author ?? "Unknown";
author.href = swfData.authorLink ?? "#";
localFileInput.value = "";
}
function hideSample() {
sampleFileInput.selectedIndex = -1;
authorContainer.classList.add("hidden");
author.textContent = "";
author.href = "";
}
async function loadFile(file: File | undefined) {
if (!file) {
return;
}
if (file.name) {
localFileName.textContent = file.name;
}
hideSample();
const data = await new Response(file).arrayBuffer();
load({ data: data, swfFileName: file.name, ...baseDemoConfig });
}
function loadSample() {
const swfData = (
sampleFileInput[
sampleFileInput.selectedIndex
] as HTMLOptionElementWithSwf
).swfData;
localFileName.textContent = "No file selected.";
if (swfData) {
showSample(swfData);
const config = swfData.config || baseDemoConfig;
load({ url: swfData.location, ...config });
} else {
hideSample();
unload();
}
}
localFileInput.addEventListener("change", (event) => {
const eventTarget = event.target as HTMLInputElement;
if (
eventTarget?.files &&
eventTarget?.files.length > 0 &&
eventTarget.files[0]
) {
loadFile(eventTarget.files[0]);
}
});
sampleFileInput.addEventListener("change", () => loadSample());
playerContainer.addEventListener("dragenter", (event) => {
event.stopPropagation();
event.preventDefault();
});
playerContainer.addEventListener("dragleave", (event) => {
event.stopPropagation();
event.preventDefault();
overlay.classList.remove("drag");
});
playerContainer.addEventListener("dragover", (event) => {
event.stopPropagation();
event.preventDefault();
overlay.classList.add("drag");
});
playerContainer.addEventListener("drop", (event) => {
event.stopPropagation();
event.preventDefault();
overlay.classList.remove("drag");
if (event.dataTransfer) {
localFileInput.files = event.dataTransfer.files;
loadFile(event.dataTransfer.files[0]);
}
});
localFileInput.addEventListener("dragleave", (event) => {
event.stopPropagation();
event.preventDefault();
overlay.classList.remove("drag");
});
localFileInput.addEventListener("dragover", (event) => {
event.stopPropagation();
event.preventDefault();
overlay.classList.add("drag");
});
localFileInput.addEventListener("drop", (event) => {
event.stopPropagation();
event.preventDefault();
overlay.classList.remove("drag");
if (event.dataTransfer) {
localFileInput.files = event.dataTransfer.files;
loadFile(event.dataTransfer.files[0]);
}
});
toggleInfo.addEventListener("click", () => {
infoContainer.classList.toggle("hidden");
player?.classList.toggle("info-container-shown");
});
reloadSwf.addEventListener("click", () => {
if (player) {
const confirmReload = confirm("Reload the current SWF?");
if (confirmReload) {
player.reload();
}
}
});
window.addEventListener("load", () => {
if (
navigator.userAgent.match(/iPad/i) ||
navigator.userAgent.match(/iPhone/i) ||
(navigator.platform === "MacIntel" &&
typeof navigator.standalone !== "undefined")
) {
localFileInput.removeAttribute("accept");
}
overlay.removeAttribute("hidden");
});
(async () => {
const response = await fetch("swfs.json");
if (response.ok) {
const data: { swfs: [DemoSwf] } = await response.json();
for (const swfData of data.swfs) {
const option = document.createElement(
"option",
) as HTMLOptionElementWithSwf;
option.textContent = swfData.title ?? "Unknown";
option.value = swfData.location;
option.swfData = swfData;
if (swfData.type) {
optionGroups[swfData.type].append(option);
} else {
sampleFileInput.insertBefore(
option,
sampleFileInput.firstChild,
);
}
}
sampleFileInputContainer.classList.remove("hidden");
}
sampleFileInput.selectedIndex = 0;
const initialFile = new URL(window.location.href).searchParams.get("file");
if (initialFile) {
const options = Array.from(sampleFileInput.options);
sampleFileInput.selectedIndex = Math.max(
options.findIndex((swfData) => swfData.value.endsWith(initialFile)),
0,
);
}
loadSample();
})();
document.getElementById("local-file-label")!.addEventListener("click", () => {
document.getElementById("local-file")!.click();
});

View File

@ -3,7 +3,7 @@
font-family: Lato;
font-style: normal;
font-weight: 400;
src: url("../fonts/S6uyw4BMUTPHjxAwXjeu.woff2") format("woff2");
src: url("fonts/S6uyw4BMUTPHjxAwXjeu.woff2") format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@ -13,7 +13,7 @@
font-family: Lato;
font-style: normal;
font-weight: 400;
src: url("../fonts/S6uyw4BMUTPHjx4wXg.woff2") format("woff2");
src: url("fonts/S6uyw4BMUTPHjx4wXg.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;

View File

@ -0,0 +1,28 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./common.css";
import "./lato.css";
import "./index.css";
import { App } from "./App.tsx";
import {
AutoPlay,
Letterbox,
LogLevel,
PublicAPI,
UnmuteOverlay,
} from "ruffle-core";
window.RufflePlayer = PublicAPI.negotiate(window.RufflePlayer, "local");
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App
autoplay={AutoPlay.On}
unmuteOverlay={UnmuteOverlay.Hidden}
logLevel={LogLevel.Warn}
letterbox={Letterbox.On}
forceScale
forceAlign
/>
</React.StrictMode>,
);

View File

@ -0,0 +1,108 @@
import { MovieMetadata } from "ruffle-core";
interface MetadataProps {
visible: boolean;
metadata: MovieMetadata | null;
}
const swfToFlashVersion: { [key: number]: string } = {
1: "1",
2: "2",
3: "3",
4: "4",
5: "5",
6: "6",
7: "7",
8: "8",
9: "9.0",
10: "10.0/10.1",
11: "10.2",
12: "10.3",
13: "11.0",
14: "11.1",
15: "11.2",
16: "11.3",
17: "11.4",
18: "11.5",
19: "11.6",
20: "11.7",
21: "11.8",
22: "11.9",
23: "12",
24: "13",
25: "14",
26: "15",
27: "16",
28: "17",
29: "18",
30: "19",
31: "20",
32: "21",
33: "22",
34: "23",
35: "24",
36: "25",
37: "26",
38: "27",
39: "28",
40: "29",
41: "30",
42: "31",
43: "32",
};
export function MetadataPanel({ visible, metadata }: MetadataProps) {
return (
<div id="info-container" className={visible ? "" : "hidden"}>
<div>
<span>Uncompressed Length</span>
<span id="uncompressedLength">
{(metadata?.uncompressedLength ?? 0) >> 10}Kb
</span>
</div>
<div>
<span>SWF Version</span>
<span id="swfVersion">{metadata?.swfVersion}</span>
</div>
<div>
<span>FP Version</span>
<span id="flashVersion">
{metadata
? swfToFlashVersion[metadata.swfVersion] ?? "Unknown"
: ""}
</span>
</div>
<div>
<span>ActionScript 3</span>
<span id="isActionScript3">
{metadata?.isActionScript3 ? "true" : "false"}
</span>
</div>
<div>
<span>Total Frames</span>
<span id="numFrames">{metadata?.numFrames}</span>
</div>
<div>
<span>Frame Rate</span>
<span id="frameRate">{metadata?.frameRate}</span>
</div>
<div>
<span>SWF Width</span>
<span id="width">{metadata?.width}</span>
</div>
<div>
<span>SWF Height</span>
<span id="height">{metadata?.height}</span>
</div>
<div>
<span>SWF Background Color</span>
<span
id="backgroundColor"
style={{
backgroundColor: metadata?.backgroundColor ?? undefined,
}}
></span>
</div>
</div>
);
}

View File

@ -0,0 +1,279 @@
import ruffleLogo from "/logo.svg";
import {
ChangeEvent,
FormEvent,
useEffect,
useRef,
useState,
DragEvent,
Fragment,
useCallback,
} from "react";
import { BaseLoadOptions } from "ruffle-core";
declare global {
interface Navigator {
/**
* iPadOS sends a User-Agent string that appears to be from macOS.
* navigator.standalone is not defined on macOS, so we use it for iPad detection.
*/
standalone?: boolean;
}
}
interface NavbarProps {
onToggleMetadata: () => void;
onReloadMovie: () => void;
onSelectUrl: (url: string, options: BaseLoadOptions) => void;
onSelectFile: (file: File) => void;
selectedFilename: string | null;
setSelectedFilename: (value: string | null) => void;
onFileDragLeave: (event: DragEvent<HTMLElement>) => void;
onFileDragOver: (event: DragEvent<HTMLElement>) => void;
onFileDragDrop: (event: DragEvent<HTMLElement>) => void;
}
interface DemoSwf {
location: string;
title?: string;
author?: string;
authorLink?: string;
config?: BaseLoadOptions;
type: "Animation" | "Game";
}
export function Navbar({
onToggleMetadata,
onReloadMovie,
onSelectUrl,
onSelectFile,
selectedFilename,
setSelectedFilename,
onFileDragLeave,
onFileDragOver,
onFileDragDrop,
}: NavbarProps) {
const localFileInput = useRef<HTMLInputElement>(null);
const urlInput = useRef<HTMLInputElement>(null);
const sampleSelectionInput = useRef<HTMLSelectElement>(null);
const [availableSamples, setAvailableSamples] = useState<DemoSwf[]>([]);
const [selectedSample, setSelectedSample] = useState<DemoSwf | null>(null);
const openFileBrowser = () => {
localFileInput.current?.click();
};
const loadUrl = (url: string) => {
onSelectUrl(url, {});
setSelectedFilename(null);
setSelectedSample(null);
sampleSelectionInput.current!.selectedIndex = -1;
};
const loadFile = (file: File) => {
onSelectFile(file);
setSelectedSample(null);
setSelectedFilename(file.name);
sampleSelectionInput.current!.selectedIndex = -1;
};
const loadSample = useCallback(
(swf: DemoSwf) => {
onSelectUrl(swf.location, swf.config ?? {});
setSelectedSample(swf);
setSelectedFilename(null);
},
[onSelectUrl, setSelectedFilename],
);
const submitUrlForm = (e: FormEvent) => {
e.preventDefault();
if (urlInput.current?.value) {
loadUrl(urlInput.current.value);
}
};
const loadSelectedFile = (e: ChangeEvent) => {
const eventTarget = e.target as HTMLInputElement;
if (
eventTarget?.files &&
eventTarget?.files.length > 0 &&
eventTarget.files[0]
) {
loadFile(eventTarget.files[0]);
}
};
const loadSelectedSample = (e: ChangeEvent) => {
const eventTarget = e.target as HTMLSelectElement;
const index = parseInt(eventTarget.value, 10);
if (availableSamples[index]) {
loadSample(availableSamples[index]);
}
};
const confirmAndReload = () => {
const confirmReload = confirm("Reload the current SWF?");
if (confirmReload) {
onReloadMovie();
}
};
const iosInputWorkaround =
navigator.userAgent.match(/iPad/i) ||
navigator.userAgent.match(/iPhone/i) ||
(navigator.platform === "MacIntel" &&
typeof navigator.standalone !== "undefined");
useEffect(() => {
(async () => {
const response = await fetch("swfs.json");
if (response.ok) {
const data: { swfs: [DemoSwf] } = await response.json();
setAvailableSamples(data.swfs);
if (data.swfs.length > 0) {
loadSample(data.swfs[0]);
}
}
})();
}, [loadSample]);
useEffect(() => {
if (selectedFilename != null) {
setSelectedSample(null);
sampleSelectionInput.current!.selectedIndex = -1;
}
}, [selectedFilename]);
return (
<div id="nav">
<a id="logo-container" href="https://ruffle.rs/" target="_blank">
<img className="logo" src={ruffleLogo} alt="Ruffle" />
</a>
<div className="select-container">
<form id="web-url-container" onSubmit={submitUrlForm}>
<input
id="web-url"
name="web-url"
type="text"
placeholder="URL of a .swf file on the web"
ref={urlInput}
/>
<button id="web-form-submit" type="submit">
Load
</button>
</form>
<div
id="local-file-container"
onDragLeave={onFileDragLeave}
onDragOver={onFileDragOver}
onDrop={onFileDragDrop}
>
<span
id="local-file-static-label"
onClick={openFileBrowser}
>
Local SWF:
</span>
<input
type="file"
accept={iosInputWorkaround ? undefined : ".swf,.spl"}
id="local-file"
aria-describedby="local-file-static-label"
ref={localFileInput}
onChange={loadSelectedFile}
/>
<button id="local-file-label" onClick={openFileBrowser}>
Select File
</button>
<label id="local-file-name" htmlFor="local-file">
{selectedFilename ?? "No file selected."}
</label>
</div>
<div
id="sample-swfs-container"
className={availableSamples.length == 0 ? "hidden" : ""}
>
<span id="sample-swfs-label">Sample SWF:</span>
<select
id="sample-swfs"
aria-describedby="sample-swfs-label"
onChange={loadSelectedSample}
ref={sampleSelectionInput}
>
{availableSamples.map((sample, i) => (
<Fragment key={i}>
{sample.type == null && (
<option key={i} value={i}>
{sample.title}
</option>
)}
</Fragment>
))}
<optgroup id="anim-optgroup" label="Animations">
{availableSamples.map((sample, i) => (
<Fragment key={i}>
{sample.type == "Animation" && (
<option key={i} value={i}>
{sample.title}
</option>
)}
</Fragment>
))}
</optgroup>
<optgroup id="games-optgroup" label="Games">
{availableSamples.map((sample, i) => (
<Fragment key={i}>
{sample.type == "Game" && (
<option key={i} value={i}>
{sample.title}
</option>
)}
</Fragment>
))}
</optgroup>
</select>
<div
id="author-container"
className={selectedSample?.author ? "" : "hidden"}
>
<span>Author: </span>
<a
href={selectedSample?.authorLink}
target="_blank"
id="author"
>
{selectedSample?.author}
</a>
</div>
</div>
</div>
<div>
<svg
id="toggle-info"
width="20px"
viewBox="0 0 416.979 416.979"
onClick={onToggleMetadata}
>
<path
fill="white"
d="M356.004 61.156c-81.37-81.47-213.377-81.551-294.848-.182-81.47 81.371-81.552 213.379-.181 294.85 81.369 81.47 213.378 81.551 294.849.181 81.469-81.369 81.551-213.379.18-294.849zM237.6 340.786a5.821 5.821 0 0 1-5.822 5.822h-46.576a5.821 5.821 0 0 1-5.822-5.822V167.885a5.821 5.821 0 0 1 5.822-5.822h46.576a5.82 5.82 0 0 1 5.822 5.822v172.901zm-29.11-202.885c-18.618 0-33.766-15.146-33.766-33.765 0-18.617 15.147-33.766 33.766-33.766s33.766 15.148 33.766 33.766c0 18.619-15.149 33.765-33.766 33.765z"
/>
</svg>
<svg
id="reload-swf"
width="20px"
viewBox="0 0 489.711 489.711"
onClick={confirmAndReload}
>
<path
fill="white"
d="M112.156 97.111c72.3-65.4 180.5-66.4 253.8-6.7l-58.1 2.2c-7.5.3-13.3 6.5-13 14 .3 7.3 6.3 13 13.5 13h.5l89.2-3.3c7.3-.3 13-6.2 13-13.5v-1.6l-3.3-88.2c-.3-7.5-6.6-13.3-14-13-7.5.3-13.3 6.5-13 14l2.1 55.3c-36.3-29.7-81-46.9-128.8-49.3-59.2-3-116.1 17.3-160 57.1-60.4 54.7-86 137.9-66.8 217.1 1.5 6.2 7 10.3 13.1 10.3 1.1 0 2.1-.1 3.2-.4 7.2-1.8 11.7-9.1 9.9-16.3-16.8-69.6 5.6-142.7 58.7-190.7zm350.3 98.4c-1.8-7.2-9.1-11.7-16.3-9.9-7.2 1.8-11.7 9.1-9.9 16.3 16.9 69.6-5.6 142.7-58.7 190.7-37.3 33.7-84.1 50.3-130.7 50.3-44.5 0-88.9-15.1-124.7-44.9l58.8-5.3c7.4-.7 12.9-7.2 12.2-14.7s-7.2-12.9-14.7-12.2l-88.9 8c-7.4.7-12.9 7.2-12.2 14.7l8 88.9c.6 7 6.5 12.3 13.4 12.3.4 0 .8 0 1.2-.1 7.4-.7 12.9-7.2 12.2-14.7l-4.8-54.1c36.3 29.4 80.8 46.5 128.3 48.9 3.8.2 7.6.3 11.3.3 55.1 0 107.5-20.2 148.7-57.4 60.4-54.7 86-137.8 66.8-217.1z"
/>
</svg>
</div>
</div>
);
}

View File

@ -0,0 +1,108 @@
import React, { ReactNode, DragEvent } from "react";
import {
PublicAPI,
RufflePlayer,
MovieMetadata,
BaseLoadOptions,
} from "ruffle-core";
export interface PlayerAttributes {
id?: string | undefined;
children?: ReactNode;
onLoadedMetadata: (metadata: MovieMetadata) => void;
baseConfig?: BaseLoadOptions;
onDragEnter: (event: DragEvent<HTMLElement>) => void;
onDragLeave: (event: DragEvent<HTMLElement>) => void;
onDragOver: (event: DragEvent<HTMLElement>) => void;
onDragDrop: (event: DragEvent<HTMLElement>) => void;
}
export class Player extends React.Component<PlayerAttributes> {
private readonly container: React.RefObject<HTMLDivElement>;
private player: RufflePlayer | null = null;
// [NA] Ruffle has a bug where if you load a swf whilst it's already loading another swf, it breaks
// Combine this with React testing everything by loading things twice to catch bugs - well, they caught the bug for sure.
// This is a hacky workaround.
private isLoading: boolean = false;
constructor(props: PlayerAttributes) {
super(props);
this.container = React.createRef();
}
componentDidMount() {
this.player = (window.RufflePlayer as PublicAPI)
.newest()!
.createPlayer()!;
this.player.id = "player";
this.player.addEventListener("loadedmetadata", () => {
if (this.props.onLoadedMetadata) {
this.props.onLoadedMetadata(this.player!.metadata!);
}
});
this.isLoading = false;
// current is guaranteed to be set before this callback
this.container.current!.appendChild(this.player);
}
componentWillUnmount() {
this.player?.remove();
this.player = null;
this.isLoading = false;
}
render() {
return (
<div
id={this.props.id}
ref={this.container}
onDragEnter={this.props.onDragEnter}
onDragLeave={this.props.onDragLeave}
onDragOver={this.props.onDragOver}
onDrop={this.props.onDragDrop}
>
{this.props.children}
</div>
);
}
reload() {
if (!this.isLoading) {
this.isLoading = true;
this.player?.reload().finally(() => {
this.isLoading = false;
});
}
}
loadUrl(url: string, options: BaseLoadOptions) {
if (!this.isLoading) {
this.isLoading = true;
this.player
?.load({ url, ...this.props.baseConfig, ...options }, false)
.finally(() => {
this.isLoading = false;
});
}
}
loadFile(file: File) {
if (!this.isLoading) {
this.isLoading = true;
new Response(file)
.arrayBuffer()
.then((data) => {
return this.player?.load(
{ data, ...this.props.baseConfig },
false,
);
})
.finally(() => {
this.isLoading = false;
});
}
}
}

1
web/packages/demo/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -1,11 +1,25 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"module": "es2020",
"moduleResolution": "node",
"target": "es2017",
"rootDir": "src",
},
"include": ["src/**/*"],
"references": [{ "path": "../core" }],
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})

View File

@ -1,60 +0,0 @@
/* eslint-env node */
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
module.exports = (_env, _argv) => {
const mode = process.env.NODE_ENV || "production";
console.log(`Building ${mode}...`);
return {
mode,
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
publicPath: "",
clean: true,
},
resolve: {
extensions: [".ts", "..."],
},
module: {
rules: [
{
test: /\.ts$/i,
use: "ts-loader",
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
performance: {
assetFilter: (assetFilename) =>
!/\.(map|wasm)$/i.test(assetFilename),
},
devServer: {
client: {
overlay: false,
},
},
devtool: "source-map",
plugins: [
new CopyPlugin({
patterns: [
{ from: path.resolve(__dirname, "www/index.html") },
{ from: path.resolve(__dirname, "www/logo-anim.swf") },
{ from: path.resolve(__dirname, "www/icon32.png") },
{ from: path.resolve(__dirname, "www/icon48.png") },
{ from: path.resolve(__dirname, "www/icon180.png") },
{ from: path.resolve(__dirname, "www/logo.svg") },
{ from: "swfs.json", noErrorOnMissing: true },
{ from: "LICENSE*" },
{ from: "README.md" },
],
}),
],
};
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,89 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ruffle Player</title>
<link rel="icon" href="icon32.png" sizes="32x32">
<link rel="icon" href="icon64.png" sizes="64x64">
<link rel="icon" href="icon180.png" sizes="180x180">
</head>
<body>
<div id="nav">
<a id="logo-container" href="https://ruffle.rs/" target="_blank">
<img class="logo" src="logo.svg" alt="Ruffle" data-canonical-src="https://ruffle.rs/assets/logo.svg" />
</a>
<div class="select-container">
<div id="web-url-container">
<input id="web-url" name="web-url" type="text" placeholder="URL of a .swf file on the web">
<button type="submit" id="web-form-submit">Load</button>
</div>
<div id="local-file-container">
<span id="local-file-static-label">Local SWF:</span>
<input type="file" accept=".swf,.spl" id="local-file" aria-describedby="local-file-static-label" />
<button id="local-file-label">Select File</button>
<label id="local-file-name" for="local-file">No file selected.</label>
</div>
<div id="sample-swfs-container" class="hidden">
<span id="sample-swfs-label">Sample SWF:</span>
<select id="sample-swfs" aria-describedby="sample-swfs-label">
<optgroup id="anim-optgroup" label="Animations"></optgroup>
<optgroup id="games-optgroup" label="Games"></optgroup>
</select>
<div id="author-container" class="hidden">
<span>Author: </span><a href="#" target="_blank" id="author"></a>
</div>
</div>
</div>
<div>
<svg id="toggle-info" width="20px" viewBox="0 0 416.979 416.979"><path fill="white" d="M356.004 61.156c-81.37-81.47-213.377-81.551-294.848-.182-81.47 81.371-81.552 213.379-.181 294.85 81.369 81.47 213.378 81.551 294.849.181 81.469-81.369 81.551-213.379.18-294.849zM237.6 340.786a5.821 5.821 0 0 1-5.822 5.822h-46.576a5.821 5.821 0 0 1-5.822-5.822V167.885a5.821 5.821 0 0 1 5.822-5.822h46.576a5.82 5.82 0 0 1 5.822 5.822v172.901zm-29.11-202.885c-18.618 0-33.766-15.146-33.766-33.765 0-18.617 15.147-33.766 33.766-33.766s33.766 15.148 33.766 33.766c0 18.619-15.149 33.765-33.766 33.765z"/></svg>
<svg id="reload-swf" width="20px" viewBox="0 0 489.711 489.711"><path fill="white" d="M112.156 97.111c72.3-65.4 180.5-66.4 253.8-6.7l-58.1 2.2c-7.5.3-13.3 6.5-13 14 .3 7.3 6.3 13 13.5 13h.5l89.2-3.3c7.3-.3 13-6.2 13-13.5v-1.6l-3.3-88.2c-.3-7.5-6.6-13.3-14-13-7.5.3-13.3 6.5-13 14l2.1 55.3c-36.3-29.7-81-46.9-128.8-49.3-59.2-3-116.1 17.3-160 57.1-60.4 54.7-86 137.9-66.8 217.1 1.5 6.2 7 10.3 13.1 10.3 1.1 0 2.1-.1 3.2-.4 7.2-1.8 11.7-9.1 9.9-16.3-16.8-69.6 5.6-142.7 58.7-190.7zm350.3 98.4c-1.8-7.2-9.1-11.7-16.3-9.9-7.2 1.8-11.7 9.1-9.9 16.3 16.9 69.6-5.6 142.7-58.7 190.7-37.3 33.7-84.1 50.3-130.7 50.3-44.5 0-88.9-15.1-124.7-44.9l58.8-5.3c7.4-.7 12.9-7.2 12.2-14.7s-7.2-12.9-14.7-12.2l-88.9 8c-7.4.7-12.9 7.2-12.2 14.7l8 88.9c.6 7 6.5 12.3 13.4 12.3.4 0 .8 0 1.2-.1 7.4-.7 12.9-7.2 12.2-14.7l-4.8-54.1c36.3 29.4 80.8 46.5 128.3 48.9 3.8.2 7.6.3 11.3.3 55.1 0 107.5-20.2 148.7-57.4 60.4-54.7 86-137.8 66.8-217.1z"/></svg>
</div>
</div>
<div id="main">
<div id="player-container" aria-label="Select a demo or drag an SWF">
<div id="overlay" hidden></div>
</div>
<div id="info-container" class="hidden">
<div>
<span>Uncompressed Length</span>
<span id="uncompressedLength">N/A</span>
</div>
<div>
<span>SWF Version</span>
<span id="swfVersion">N/A</span>
</div>
<div>
<span>FP Version</span>
<span id="flashVersion">N/A</span>
</div>
<div>
<span>ActionScript 3</span>
<span id="isActionScript3">N/A</span>
</div>
<div>
<span>Total Frames</span>
<span id="numFrames">N/A</span>
</div>
<div>
<span>Frame Rate</span>
<span id="frameRate">N/A</span>
</div>
<div>
<span>SWF Width</span>
<span id="width">N/A</span>
</div>
<div>
<span>SWF Height</span>
<span id="height">N/A</span>
</div>
<div>
<span>SWF Background Color</span>
<span id="backgroundColor"></span>
</div>
</div>
</div>
<script src="index.js"></script>
</body>
</html>