web: Fix config with `serde-wasm-bindgen`

Since `serde-wasm-bindgen` doesn't support `#[serde(default)]` (https://github.com/cloudflare/serde-wasm-bindgen/issues/20),
we no longer able to deserialize a partial `Config` object. As a solution,
take care to pass a full object from the TypeScript side.
This commit is contained in:
relrelb 2022-09-26 01:57:43 +03:00 committed by relrelb
parent 51c9e3714a
commit a8f869329e
6 changed files with 113 additions and 96 deletions

View File

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
///
/// When letterboxed, black bars will be rendered around the exterior
/// margins of the content.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Collect, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Collect, Serialize, Deserialize)]
#[collect(require_static)]
#[serde(rename = "letterbox")]
pub enum Letterbox {
@ -16,7 +16,6 @@ pub enum Letterbox {
/// The content will only be letterboxed if the content is running fullscreen.
#[serde(rename = "fullscreen")]
#[default]
Fullscreen,
/// The content will always be letterboxed.

View File

@ -1,7 +1,11 @@
/**
* Represents the various types of auto-play behaviours that are supported.
*/
import type { BaseLoadOptions } from "./load-options";
import {
AutoPlay,
UnmuteOverlay,
WindowMode,
Letterbox,
LogLevel,
} from "./load-options";
/**
* The configuration object to control Ruffle's behaviour on the website
@ -10,8 +14,10 @@ import type { BaseLoadOptions } from "./load-options";
export interface Config extends BaseLoadOptions {
/**
* The URL at which Ruffle can load its extra files (i.e. `.wasm`).
*
* @default null
*/
publicPath?: string;
publicPath?: string | null;
/**
* Whether or not to enable polyfills on the page.
@ -24,3 +30,27 @@ export interface Config extends BaseLoadOptions {
*/
polyfills?: boolean;
}
export const DEFAULT_CONFIG: Required<Config> = {
allowScriptAccess: false,
parameters: {},
autoplay: AutoPlay.Auto,
backgroundColor: null,
letterbox: Letterbox.Fullscreen,
unmuteOverlay: UnmuteOverlay.Visible,
upgradeToHttps: true,
warnOnUnsupportedContent: true,
logLevel: LogLevel.Error,
showSwfDownload: false,
contextMenu: true,
preloader: true,
maxExecutionDuration: { secs: 15, nanos: 0 },
base: null,
menu: true,
salign: "",
quality: "high",
scale: "showAll",
wmode: WindowMode.Opaque,
publicPath: null,
polyfills: true,
};

View File

@ -1,3 +1,6 @@
/**
* Represents the various types of auto-play behaviours that are supported.
*/
export const enum AutoPlay {
/**
* The player should automatically play the movie as soon as it is loaded.
@ -135,6 +138,8 @@ export interface BaseLoadOptions {
* If a URL if specified when loading the movie, some parameters will
* be extracted by the query portion of that URL and then overwritten
* by any explicitly set here.
*
* @default {}
*/
parameters?: URLSearchParams | string | Record<string, string>;
@ -225,7 +230,7 @@ export interface BaseLoadOptions {
* Maximum amount of time a script can take before scripting
* is disabled.
*
* @default {"secs": 15, "nanos": 0}
* @default { secs: 15, nanos: 0 }
*/
maxExecutionDuration?: {
secs: number;
@ -250,7 +255,6 @@ export interface BaseLoadOptions {
menu?: boolean;
/**
*
* This is equivalent to Stage.align.
*
* @default ""
@ -258,7 +262,6 @@ export interface BaseLoadOptions {
salign?: string;
/**
*
* This is equivalent to Stage.quality.
*
* @default "high"
@ -266,7 +269,6 @@ export interface BaseLoadOptions {
quality?: string;
/**
*
* This is equivalent to Stage.scaleMode.
*
* @default "showAll"

View File

@ -44,7 +44,7 @@ try {
export function publicPath(config: Config): string {
// Default to the directory where this script resides.
let path = currentScriptURL;
if (config !== undefined && config.publicPath !== undefined) {
if (config.publicPath !== null && config.publicPath !== undefined) {
path = config.publicPath;
}

View File

@ -1,11 +1,10 @@
import type { Ruffle } from "../pkg/ruffle_web";
import { loadRuffle } from "./load-ruffle";
import { ruffleShadowTemplate } from "./shadow-template";
import { lookupElement } from "./register-element";
import type { Config } from "./config";
import { DEFAULT_CONFIG } from "./config";
import {
BaseLoadOptions,
DataLoadOptions,
URLLoadOptions,
AutoPlay,
@ -127,24 +126,21 @@ export class RufflePlayer extends HTMLElement {
// Firefox has a read-only "contextMenu" property,
// so avoid shadowing it.
private contextMenuElement: HTMLElement;
private hasContextMenu = false;
// Allows the user to permanently disable the context menu.
private contextMenuForceDisabled = false;
// Whether to show a preloader while Ruffle is still loading the SWF
private hasPreloader = true;
// Whether this device is a touch device.
// Set to true when a touch event is encountered.
private isTouch = false;
// The effective config loaded upon `.load()`.
private loadedConfig: Required<Config> = DEFAULT_CONFIG;
private swfUrl?: URL;
private instance: Ruffle | null;
private options: BaseLoadOptions | null;
private lastActivePlayingState: boolean;
private showSwfDownload = false;
private _metadata: MovieMetadata | null;
private _readyState: ReadyState;
@ -237,7 +233,6 @@ export class RufflePlayer extends HTMLElement {
window.addEventListener("click", this.hideContextMenu.bind(this));
this.instance = null;
this.options = null;
this.onFSCommand = null;
this._readyState = ReadyState.HaveNothing;
@ -395,14 +390,14 @@ export class RufflePlayer extends HTMLElement {
*
* @private
*/
private async ensureFreshInstance(config: BaseLoadOptions): Promise<void> {
private async ensureFreshInstance(): Promise<void> {
this.destroy();
if (this.hasPreloader) {
if (this.loadedConfig.preloader !== false) {
this.showPreloader();
}
const ruffleConstructor = await loadRuffle(
config,
this.loadedConfig,
this.onRuffleDownloadProgress.bind(this)
).catch((e) => {
console.error(`Serious error loading Ruffle: ${e}`);
@ -446,7 +441,7 @@ export class RufflePlayer extends HTMLElement {
this.instance = await new ruffleConstructor(
this.container,
this,
config
this.loadedConfig
);
console.log(
"New Ruffle instance created (WebAssembly extensions: " +
@ -469,17 +464,17 @@ export class RufflePlayer extends HTMLElement {
this.unmuteAudioContext();
// Treat unspecified and invalid values as `AutoPlay.Auto`.
// Treat invalid values as `AutoPlay.Auto`.
if (
config.autoplay === AutoPlay.On ||
(config.autoplay !== AutoPlay.Off &&
this.loadedConfig.autoplay === AutoPlay.On ||
(this.loadedConfig.autoplay !== AutoPlay.Off &&
this.audioState() === "running")
) {
this.play();
if (this.audioState() !== "running") {
// Treat unspecified and invalid values as `UnmuteOverlay.Visible`.
if (config.unmuteOverlay !== UnmuteOverlay.Hidden) {
// Treat invalid values as `UnmuteOverlay.Visible`.
if (this.loadedConfig.unmuteOverlay !== UnmuteOverlay.Hidden) {
this.unmuteOverlay.style.display = "block";
}
@ -541,6 +536,39 @@ export class RufflePlayer extends HTMLElement {
}
}
private checkOptions(
options: string | URLLoadOptions | DataLoadOptions
): URLLoadOptions | DataLoadOptions {
if (typeof options === "string") {
return { url: options };
}
const check: (
condition: boolean,
message: string
) => asserts condition = (condition, message) => {
if (!condition) {
const error = new TypeError(message);
error.ruffleIndexError = PanicError.JavascriptConfiguration;
this.panic(error);
throw error;
}
};
check(
options !== null && typeof options === "object",
"Argument 0 must be a string or object"
);
check(
"url" in options || "data" in options,
"Argument 0 must contain a `url` or `data` key"
);
check(
!("url" in options) || typeof options.url === "string",
"`url` must be a string"
);
return options;
}
/**
* Loads a specified movie into this player.
*
@ -557,34 +585,7 @@ export class RufflePlayer extends HTMLElement {
async load(
options: string | URLLoadOptions | DataLoadOptions
): Promise<void> {
let optionsError = "";
switch (typeof options) {
case "string":
options = { url: options };
break;
case "object":
if (options === null) {
optionsError = "Argument 0 must be a string or object";
} else if (!("url" in options) && !("data" in options)) {
optionsError =
"Argument 0 must contain a `url` or `data` key";
} else if (
"url" in options &&
typeof options.url !== "string"
) {
optionsError = "`url` must be a string";
}
break;
default:
optionsError = "Argument 0 must be a string or object";
break;
}
if (optionsError.length > 0) {
const error = new TypeError(optionsError);
error.ruffleIndexError = PanicError.JavascriptConfiguration;
this.panic(error);
throw error;
}
options = this.checkOptions(options);
if (!this.isConnected || this.isUnusedFallbackObject()) {
console.warn(
@ -599,28 +600,27 @@ export class RufflePlayer extends HTMLElement {
}
try {
const config: BaseLoadOptions = {
this.loadedConfig = {
...DEFAULT_CONFIG,
...(window.RufflePlayer?.config ?? {}),
...this.config,
...options,
};
// `allowScriptAccess` can only be set in `options`.
config.allowScriptAccess = options.allowScriptAccess;
this.showSwfDownload = config.showSwfDownload === true;
this.options = options;
this.hasContextMenu = config.contextMenu !== false;
this.hasPreloader = config.preloader !== false;
// `allowScriptAccess` can only be set in `options`.
this.loadedConfig.allowScriptAccess =
options.allowScriptAccess === true;
// Pre-emptively set background color of container while Ruffle/SWF loads.
if (
config.backgroundColor &&
config.wmode !== WindowMode.Transparent
this.loadedConfig.backgroundColor &&
this.loadedConfig.wmode !== WindowMode.Transparent
) {
this.container.style.backgroundColor = config.backgroundColor;
this.container.style.backgroundColor =
this.loadedConfig.backgroundColor;
}
await this.ensureFreshInstance(config);
await this.ensureFreshInstance();
if ("url" in options) {
console.log(`Loading SWF file ${options.url}`);
@ -844,7 +844,11 @@ export class RufflePlayer extends HTMLElement {
}
}
if (this.instance && this.swfUrl && this.showSwfDownload) {
if (
this.instance &&
this.swfUrl &&
this.loadedConfig.showSwfDownload === true
) {
items.push(null);
items.push({
text: "Download .swf",
@ -882,7 +886,10 @@ export class RufflePlayer extends HTMLElement {
private showContextMenu(e: MouseEvent): void {
e.preventDefault();
if (!this.hasContextMenu || this.contextMenuForceDisabled) {
if (
this.loadedConfig.contextMenu === false ||
this.contextMenuForceDisabled
) {
return;
}
@ -1515,9 +1522,7 @@ export class RufflePlayer extends HTMLElement {
}
protected debugPlayerInfo(): string {
let result = `Allows script access: ${
this.options?.allowScriptAccess ?? false
}\n`;
let result = `Allows script access: ${this.loadedConfig.allowScriptAccess}\n`;
if (this.instance) {
result += `Renderer: ${this.instance.renderer_name()}\n`;
}

View File

@ -117,8 +117,7 @@ struct JavascriptInterface {
}
#[derive(Serialize, Deserialize)]
#[serde(default = "Default::default")]
pub struct Config {
struct Config {
#[serde(rename = "allowScriptAccess")]
allow_script_access: bool,
@ -154,26 +153,6 @@ pub struct Config {
max_execution_duration: Duration,
}
impl Default for Config {
fn default() -> Self {
Self {
allow_script_access: false,
show_menu: true,
salign: Some("".to_owned()),
quality: Some("high".to_owned()),
scale: Some("showAll".to_owned()),
wmode: Some("opaque".to_owned()),
background_color: Default::default(),
letterbox: Default::default(),
upgrade_to_https: true,
base_url: None,
warn_on_unsupported_content: true,
log_level: log::Level::Error,
max_execution_duration: Duration::from_secs(15),
}
}
}
/// Metadata about the playing SWF file to be passed back to JavaScript.
#[derive(Serialize)]
struct MovieMetadata {
@ -205,8 +184,10 @@ impl Ruffle {
#[allow(clippy::new_ret_no_self)]
#[wasm_bindgen(constructor)]
pub fn new(parent: HtmlElement, js_player: JavascriptPlayer, config: JsValue) -> Promise {
let config: Config = serde_wasm_bindgen::from_value(config).unwrap_or_default();
wasm_bindgen_futures::future_to_promise(async move {
let config: Config = serde_wasm_bindgen::from_value(config)
.map_err(|e| format!("Error parsing config: {}", e))?;
if RUFFLE_GLOBAL_PANIC.is_completed() {
// If an actual panic happened, then we can't trust the state it left us in.
// Prevent future players from loading so that they can inform the user about the error.