extension: Make the player page a local Ruffle demo

This commit is contained in:
Daniel Jacobs 2023-08-18 17:04:50 -04:00 committed by Nathan Adams
parent a11725c27c
commit e7df6d890e
9 changed files with 540 additions and 20 deletions

View File

@ -540,7 +540,7 @@ export interface DataLoadOptions extends BaseLoadOptions {
/** /**
* The data to load a movie from. * The data to load a movie from.
*/ */
data: Iterable<number>; data: ArrayLike<number> | ArrayBufferLike;
/** /**
* The filename of the SWF movie to provide to ActionScript. * The filename of the SWF movie to provide to ActionScript.

View File

@ -150,7 +150,7 @@ export class RufflePlayer extends HTMLElement {
private contextMenuSupported = false; private contextMenuSupported = false;
// The effective config loaded upon `.load()`. // The effective config loaded upon `.load()`.
private loadedConfig?: URLLoadOptions | DataLoadOptions; public loadedConfig?: URLLoadOptions | DataLoadOptions;
private swfUrl?: URL; private swfUrl?: URL;
private instance: Ruffle | null; private instance: Ruffle | null;

View File

@ -77,6 +77,9 @@
"action_reload": { "action_reload": {
"message": "Reload tab to apply changes" "message": "Reload tab to apply changes"
}, },
"open_player_page": {
"message": "Open SWF Player"
},
"open_settings_page": { "open_settings_page": {
"message": "Open Settings Page" "message": "Open Settings Page"
}, },

View File

@ -1,6 +1,199 @@
:root {
--ruffle-blue: #37528c;
--ruffle-orange: #ffad33;
--splash-screen-background: #31497d;
}
body {
position: absolute;
inset: 0;
padding: 0;
margin: 0;
font-family: Lato, sans-serif;
display: flex;
flex-flow: column;
background: black;
}
#main {
position: relative;
flex: 1;
}
#overlay {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
border: 8px dashed var(--ruffle-orange);
border-radius: 30px;
opacity: 0;
transition: opacity 0.3s ease-in;
margin: 10px 5px;
}
#overlay.drag {
opacity: 1;
transition-timing-function: ease-out;
}
#player { #player {
position: absolute; position: absolute;
inset: 0; inset: 0;
width: auto; width: auto;
height: auto; height: auto;
margin: 10px 0;
}
#nav {
width: 100%;
background: var(--ruffle-blue);
box-shadow: 0 3px 6px 5px var(--ruffle-blue);
display: flex;
align-items: center;
justify-content: space-around;
color: white;
padding: 10px 0 5px;
margin-bottom: 5px;
}
#title {
transition: opacity 0.5s;
}
#title:hover {
opacity: 0.5;
}
#title img {
height: 32px;
}
#file-picker select,
#file-picker input,
#author {
margin-left: 5px;
}
#local-file-container,
#sample-swfs-container {
display: inline-block;
vertical-align: middle;
}
#local-file {
width: 0;
opacity: 0;
position: absolute;
}
#local-file-label {
color: var(--ruffle-blue);
padding: 3px 10px;
margin: 5px 2px;
cursor: pointer;
border-radius: 50px;
background-color: white;
}
#local-file-name {
min-width: 150px;
display: inline-block;
font-size: smaller;
}
#sample-swfs {
background-color: white;
color: var(--ruffle-blue);
border: 1px solid white;
border-radius: 5px;
}
#author-container {
font-size: small;
}
#author {
color: var(--ruffle-orange);
}
.hidden {
display: none !important;
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.modal-content {
background-color: var(--ruffle-blue);
margin: 15vh auto;
padding: 20px;
border: 2px solid white;
width: 300px;
height: 270px;
overflow: auto;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
#open-modal,
#reload-swf {
vertical-align: middle;
cursor: pointer;
}
#metadata {
margin: 0 auto;
}
#metadata td {
padding: 2px 1px;
border: 1px solid #ddd;
color: var(--ruffle-orange);
}
#metadata tr td:nth-child(1) {
font-weight: bold;
padding: 0 10px;
}
@media only screen and (width <= 800px) {
#local-file-container,
#sample-swfs-container {
display: block;
}
#local-file-container {
margin-bottom: 10px;
}
}
@media only screen and (width <= 600px) {
#local-file-static-label,
#sample-swfs-label {
display: block;
margin-bottom: 5px;
}
#author-container {
font-size: 12px;
text-align: center;
}
#nav {
flex-flow: column;
}
} }

View File

@ -4,12 +4,86 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ruffle</title> <title>Ruffle Player</title>
<link rel="icon" type="image/png" href="images/icon32.png" /> <link rel="icon" type="image/png" href="images/icon32.png" />
<link rel="stylesheet" href="css/player.css" /> <link rel="stylesheet" href="css/player.css" />
<link rel="stylesheet" href="css/index.css">
</head> </head>
<body> <body>
<div id="main"></div> <div id="nav">
<div id="title">
<a href="https://ruffle.rs/" target="_blank">
<img
src="images/logo.svg"
alt="Ruffle"
data-canonical-src="https://ruffle.rs/assets/logo.svg" />
</a>
</div>
<div id="file-picker">
<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" />
<label for="local-file" id="local-file-label">Select File</label>
<span id="local-file-name">No file selected.</span>
</div>
<div id="sample-swfs-container">
<label for="webURL">Web URL: </label>
<input id="webURL" name="webURL" type="text" placeholder="URL of a .swf file on the web">
<button type="submit" id="webFormSubmit">Load</button>
</div>
&emsp;
<svg width="20px" id="open-modal" 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"/></g></svg>
&ensp;
<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" aria-label="Select a demo or drag an SWF">
<div id="overlay" class="hidden"></div>
</div>
<div id="metadata-modal" class="modal">
<div class="modal-content">
<span class="close" id="close-modal">×</span>
<table id="metadata">
<tr>
<td>Uncompressed Length</td>
<td><span class="metadata" id="uncompressedLength">Loading</span></td>
</tr>
<tr>
<td>SWF Version</td>
<td><span class="metadata" id="swfVersion">Loading</span></td>
</tr>
<tr>
<td>FP Version</td>
<td><span class="metadata" id="flashVersion">Loading</span></td>
</tr>
<tr>
<td>ActionScript 3</td>
<td><span class="metadata" id="isActionScript3">Loading</span></td>
</tr>
<tr>
<td>Total Frames</td>
<td><span class="metadata" id="numFrames">Loading</span></td>
</tr>
<tr>
<td>Frame Rate</td>
<td><span class="metadata" id="frameRate">Loading</span></td>
</tr>
<tr>
<td>SWF Width</td>
<td><span class="metadata" id="width">Loading</span></td>
</tr>
<tr>
<td>SWF Height</td>
<td><span class="metadata" id="height">Loading</span></td>
</tr>
<tr>
<td>SWF Background Color</td>
<td><input class="metadata" type="color" id="backgroundColor" disabled value="#FFFFFF"></td>
</tr>
</table>
</div>
</div>
<script src="dist/player.js"></script> <script src="dist/player.js"></script>
</body> </body>
</html> </html>

View File

@ -38,6 +38,7 @@
</div> </div>
<div id="version-text">Ruffle extension</div> <div id="version-text">Ruffle extension</div>
<button id="options-button">Settings</button> <button id="options-button">Settings</button>
<button id="player-button">Open SWF Player</button>
<button id="reload-button" disabled>Reload</button> <button id="reload-button" disabled>Reload</button>
<script src="dist/popup.js"></script> <script src="dist/popup.js"></script>
</body> </body>

View File

@ -1,19 +1,230 @@
import * as utils from "./utils"; import * as utils from "./utils";
import { PublicAPI } from "ruffle-core"; import { PublicAPI } from "ruffle-core";
import type { Letterbox } from "ruffle-core"; import type {
Letterbox,
RufflePlayer,
DataLoadOptions,
URLLoadOptions,
} from "ruffle-core";
const api = PublicAPI.negotiate(window.RufflePlayer!, "local"); const api = PublicAPI.negotiate(window.RufflePlayer!, "local");
window.RufflePlayer = api; window.RufflePlayer = api;
const ruffle = api.newest()!; const ruffle = api.newest()!;
let player: RufflePlayer;
window.addEventListener("DOMContentLoaded", async () => { const main = document.getElementById("main")!;
const url = new URL(window.location.href); const overlay = document.getElementById("overlay")!;
// Hash always starts with #, gotta slice that off const localFileInput = document.getElementById(
const swfUrl = url.hash.length > 1 ? url.hash.slice(1) : null; "local-file",
if (!swfUrl) { )! as HTMLInputElement;
const localFileName = document.getElementById("local-file-name")!;
const closeModal = document.getElementById("close-modal")!;
const openModal = document.getElementById("open-modal")!;
const reloadSwf = document.getElementById("reload-swf")!;
const metadataModal = document.getElementById("metadata-modal")!;
// Default config used by the player.
const defaultConfig = {
letterbox: "on" as Letterbox,
forceScale: true,
forceAlign: true,
};
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",
};
function unload() {
if (player) {
player.remove();
document.querySelectorAll("span.metadata").forEach((el) => {
el.textContent = "Loading";
});
(
document.getElementById("backgroundColor")! as HTMLInputElement
).value = "#FFFFFF";
}
}
function load(options: string | DataLoadOptions | URLLoadOptions) {
unload();
player = ruffle.createPlayer();
player.id = "player";
main.append(player);
player.load(options);
player.addEventListener("loadedmetadata", function () {
if (player.metadata) {
for (const [key, value] of Object.entries(player.metadata)) {
const metadataElement = document.getElementById(key);
if (metadataElement) {
switch (key) {
case "backgroundColor":
(metadataElement as HTMLInputElement).value =
value ?? "#FFFFFF";
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;
}
}
}
}
});
}
async function loadFile(file: File | undefined) {
if (!file) {
return; return;
} }
if (file.name) {
localFileName.textContent = file.name;
}
const data = await new Response(file).arrayBuffer();
load({ data: data, swfFileName: file.name, ...defaultConfig });
history.pushState("", document.title, window.location.pathname);
}
localFileInput.addEventListener("change", (event) => {
const eventTarget = event.target as HTMLInputElement;
if (
eventTarget?.files &&
eventTarget?.files.length > 0 &&
eventTarget.files[0]
) {
loadFile(eventTarget.files[0]);
}
});
main.addEventListener("dragenter", (event) => {
event.stopPropagation();
event.preventDefault();
});
main.addEventListener("dragleave", (event) => {
event.stopPropagation();
event.preventDefault();
overlay.classList.remove("drag");
});
main.addEventListener("dragover", (event) => {
event.stopPropagation();
event.preventDefault();
overlay.classList.add("drag");
});
main.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]);
}
});
closeModal.addEventListener("click", () => {
metadataModal.style.display = "none";
});
openModal.addEventListener("click", () => {
metadataModal.style.display = "block";
});
reloadSwf.addEventListener("click", () => {
if (player) {
const confirmReload = confirm("Reload the current SWF?");
if (confirmReload) {
if (player.loadedConfig) {
player.load(player.loadedConfig);
}
}
}
});
window.addEventListener("load", () => {
if (
navigator.userAgent.match(/iPad/i) ||
navigator.userAgent.match(/iPhone/i)
) {
localFileInput.removeAttribute("accept");
}
overlay.classList.remove("hidden");
});
window.onclick = (event) => {
if (event.target === metadataModal) {
metadataModal.style.display = "none";
}
};
async function loadSwf(swfUrl: string) {
try { try {
const pathname = new URL(swfUrl).pathname; const pathname = new URL(swfUrl).pathname;
document.title = pathname.substring(pathname.lastIndexOf("/") + 1); document.title = pathname.substring(pathname.lastIndexOf("/") + 1);
@ -21,19 +232,39 @@ window.addEventListener("DOMContentLoaded", async () => {
// Ignore URL parsing errors. // Ignore URL parsing errors.
} }
const player = ruffle.createPlayer();
player.id = "player";
document.getElementById("main")!.append(player);
const options = await utils.getExplicitOptions(); const options = await utils.getExplicitOptions();
localFileName.textContent = document.title;
player.load({ localFileInput.value = "";
load({
...options, ...options,
url: swfUrl, url: swfUrl,
base: swfUrl.substring(0, swfUrl.lastIndexOf("/") + 1), base: swfUrl.substring(0, swfUrl.lastIndexOf("/") + 1),
// Override some default values when playing in the extension player page. ...defaultConfig,
letterbox: "on" as Letterbox,
forceAlign: true,
forceScale: true,
}); });
}
window.addEventListener("pageshow", async () => {
const url = new URL(window.location.href);
// Hash always starts with #, gotta slice that off
const swfUrl = url.hash.length > 1 ? url.hash.slice(1) : null;
if (swfUrl) {
await loadSwf(swfUrl);
}
});
window.addEventListener("DOMContentLoaded", async () => {
const webFormSubmit = document.getElementById(
"webFormSubmit",
) as HTMLButtonElement;
if (webFormSubmit) {
webFormSubmit.addEventListener("click", function () {
const webURL = document.getElementById(
"webURL",
) as HTMLInputElement;
if ((webURL?.value || "") !== "") {
loadSwf(webURL.value);
window.location.href = "#" + webURL.value;
}
});
}
}); });

View File

@ -170,6 +170,15 @@ window.addEventListener("DOMContentLoaded", () => {
window.close(); window.close();
}); });
const playerButton = document.getElementById(
"player-button",
) as HTMLButtonElement;
playerButton.textContent = utils.i18n.getMessage("open_player_page");
playerButton.addEventListener("click", async () => {
await utils.openPlayerPage();
window.close();
});
reloadButton = document.getElementById( reloadButton = document.getElementById(
"reload-button", "reload-button",
) as HTMLButtonElement; ) as HTMLButtonElement;

View File

@ -63,6 +63,7 @@ export let runtime: {
}; };
export let openOptionsPage: () => Promise<void>; export let openOptionsPage: () => Promise<void>;
export let openPlayerPage: () => Promise<void>;
function promisify<T>( function promisify<T>(
func: (callback: (result: T) => void) => void, func: (callback: (result: T) => void) => void,
@ -129,12 +130,20 @@ if (typeof chrome !== "undefined") {
promisify((cb: () => void) => promisify((cb: () => void) =>
chrome.tabs.create({ url: "/options.html" }, cb), chrome.tabs.create({ url: "/options.html" }, cb),
); );
openPlayerPage = () =>
promisify((_cb: () => void) =>
chrome.tabs.create({ url: "/player.html" }),
);
} else if (typeof browser !== "undefined") { } else if (typeof browser !== "undefined") {
i18n = browser.i18n; i18n = browser.i18n;
storage = browser.storage; storage = browser.storage;
tabs = browser.tabs; tabs = browser.tabs;
runtime = browser.runtime; runtime = browser.runtime;
openOptionsPage = () => browser.runtime.openOptionsPage(); openOptionsPage = () => browser.runtime.openOptionsPage();
openPlayerPage = () =>
promisify((_cb: () => void) =>
browser.tabs.create({ url: "/player.html" }),
);
} else { } else {
throw new Error("Extension API not found."); throw new Error("Extension API not found.");
} }