web: Rewrite demo in react
|
@ -44,7 +44,7 @@
|
||||||
"build:debug": "cross-env NODE_ENV=development CARGO_FEATURES=avm_debug npm run build",
|
"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: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",
|
"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",
|
"test": "npm test --workspaces --if-present",
|
||||||
"docs": "npm run docs --workspaces --if-present",
|
"docs": "npm run docs --workspaces --if-present",
|
||||||
"lint": "eslint . && stylelint **.css",
|
"lint": "eslint . && stylelint **.css",
|
||||||
|
|
|
@ -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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
env:
|
|
||||||
browser: true
|
|
||||||
parserOptions:
|
|
||||||
sourceType: module
|
|
|
@ -1 +1 @@
|
||||||
/swfs.json
|
/public/swfs.json
|
||||||
|
|
|
@ -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.
|
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.
|
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
|
### Configuring the demo
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -4,17 +4,28 @@
|
||||||
"description": "Demo of Ruffle Flash emulator",
|
"description": "Demo of Ruffle Flash emulator",
|
||||||
"license": "(MIT OR Apache-2.0)",
|
"license": "(MIT OR Apache-2.0)",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"dev": "vite",
|
||||||
"start": "webpack serve"
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
"ruffle-core": "^0.1.0"
|
"ruffle-core": "^0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"css-loader": "^6.8.1",
|
"@types/react": "^18.2.37",
|
||||||
"style-loader": "^3.3.3",
|
"@types/react-dom": "^18.2.15",
|
||||||
"ts-loader": "^9.5.0",
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
"webpack-cli": "^5.1.4"
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
body {
|
#root {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -103,6 +103,10 @@ body {
|
||||||
transition-timing-function: ease-out;
|
transition-timing-function: ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-container-shown #player {
|
||||||
|
width: calc(100% - 300px);
|
||||||
|
}
|
||||||
|
|
||||||
#overlay:not([hidden]) ~ #player {
|
#overlay:not([hidden]) ~ #player {
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
}
|
}
|
||||||
|
@ -119,10 +123,6 @@ body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player.info-container-shown {
|
|
||||||
width: calc(100% - 300px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#info-container span:first-child {
|
#info-container span:first-child {
|
||||||
text-shadow: 0 0 1px white;
|
text-shadow: 0 0 1px white;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
|
@ -3,7 +3,7 @@
|
||||||
font-family: Lato;
|
font-family: Lato;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
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,
|
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;
|
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
font-family: Lato;
|
font-family: Lato;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
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,
|
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+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;
|
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
|
|
@ -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>,
|
||||||
|
);
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -1,11 +1,25 @@
|
||||||
{
|
{
|
||||||
"extends": "@tsconfig/strictest/tsconfig.json",
|
"compilerOptions": {
|
||||||
"compilerOptions": {
|
"target": "ES2020",
|
||||||
"module": "es2020",
|
"useDefineForClassFields": true,
|
||||||
"moduleResolution": "node",
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"target": "es2017",
|
"module": "ESNext",
|
||||||
"rootDir": "src",
|
"skipLibCheck": true,
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
/* Bundler mode */
|
||||||
"references": [{ "path": "../core" }],
|
"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" }]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
|
@ -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" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
Before Width: | Height: | Size: 2.1 KiB |
|
@ -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>
|
|