web: Rewrite demo in react
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
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;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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-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;
|
||||
|
|
|
@ -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": {
|
||||
"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" }]
|
||||
}
|
||||
|
|
|
@ -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>
|