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:
parent
51c9e3714a
commit
a8f869329e
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue