diff --git a/web/packages/extension/assets/css/common.css b/web/packages/extension/assets/css/common.css
index 8c3beb54e06f..2c370689bbc3 100644
--- a/web/packages/extension/assets/css/common.css
+++ b/web/packages/extension/assets/css/common.css
@@ -157,3 +157,19 @@ button {
height: 20px;
margin: auto;
}
+
+/* Textarea input */
+.option.textarea {
+ flex-direction: column;
+}
+
+.option.textarea label {
+ color: var(--ruffle-orange);
+ font-size: 20px;
+ margin: 8px auto;
+ padding: 0;
+}
+
+.option.textarea textarea {
+ width: 100%;
+}
diff --git a/web/packages/extension/assets/options.html b/web/packages/extension/assets/options.html
index 6431a4121f44..51a7cec45611 100644
--- a/web/packages/extension/assets/options.html
+++ b/web/packages/extension/assets/options.html
@@ -66,6 +66,11 @@
+
+
+
+
+
diff --git a/web/packages/extension/src/common.ts b/web/packages/extension/src/common.ts
index 4dec1094742b..994e925fee9f 100644
--- a/web/packages/extension/src/common.ts
+++ b/web/packages/extension/src/common.ts
@@ -7,11 +7,13 @@ export interface Options extends Config.BaseLoadOptions {
autostart: boolean;
showReloadButton: boolean;
swfTakeover: boolean;
+ customConfig?: string;
}
interface OptionElement {
readonly input: Element;
readonly label: HTMLLabelElement;
+ readonly submitBtn?: HTMLButtonElement;
value: T;
}
@@ -110,6 +112,26 @@ class SelectOption implements OptionElement {
}
}
+class TextareaOption implements OptionElement {
+ constructor(
+ private readonly textarea: HTMLTextAreaElement,
+ readonly label: HTMLLabelElement,
+ readonly submitBtn: HTMLButtonElement,
+ ) {}
+
+ get input() {
+ return this.textarea;
+ }
+
+ get value() {
+ return this.textarea.value;
+ }
+
+ set value(value: string) {
+ this.textarea.value = value ?? "";
+ }
+}
+
function getElement(option: Element): OptionElement {
const label = option.getElementsByTagName("label")[0]!;
@@ -129,6 +151,12 @@ function getElement(option: Element): OptionElement {
return new SelectOption(select, label);
}
+ const [textarea] = option.getElementsByTagName("textarea");
+ if (textarea) {
+ const submitBtn = option.getElementsByTagName("button")[0]!;
+ return new TextareaOption(textarea, label, submitBtn);
+ }
+
throw new Error("Unknown option element");
}
@@ -168,12 +196,30 @@ export async function bindOptions(
element.label.textContent = message;
}
- // Listen for user input.
- element.input.addEventListener("change", () => {
- const value = element.value;
- options[key] = value as never;
- utils.storage.sync.set({ [key]: value });
- });
+ if (element.input.nodeName === "TEXTAREA" && element.submitBtn) {
+ element.submitBtn.addEventListener("click", () => {
+ const value = element.value as string;
+ if (value.trim() === "") {
+ utils.storage.sync.set({ [key]: "" });
+ alert("Custom configuration cleared.");
+ return;
+ }
+ try {
+ JSON.parse(value);
+ utils.storage.sync.set({ [key]: value });
+ alert("Custom configuration saved successfully.");
+ } catch (_e) {
+ alert("Invalid configuration.");
+ }
+ });
+ } else {
+ // Listen for user input.
+ element.input.addEventListener("change", () => {
+ const value = element.value;
+ options[key] = value as never;
+ utils.storage.sync.set({ [key]: value });
+ });
+ }
}
// Listen for future changes.
diff --git a/web/packages/extension/src/content.ts b/web/packages/extension/src/content.ts
index a6ab01115ad6..eac7cbb76cf4 100644
--- a/web/packages/extension/src/content.ts
+++ b/web/packages/extension/src/content.ts
@@ -191,10 +191,10 @@ function isXMLDocument(): boolean {
await sendMessageToPage({
type: "load",
config: {
- ...explicitOptions,
autoplay: options.autostart ? "on" : "auto",
unmuteOverlay: options.autostart ? "hidden" : "visible",
splashScreen: !options.autostart,
+ ...explicitOptions,
},
publicPath: utils.runtime.getURL("/dist/"),
});
diff --git a/web/packages/extension/src/utils.ts b/web/packages/extension/src/utils.ts
index 35815cf2ff55..58f4ffffaad0 100644
--- a/web/packages/extension/src/utils.ts
+++ b/web/packages/extension/src/utils.ts
@@ -8,6 +8,7 @@ const DEFAULT_OPTIONS: Required = {
autostart: false,
showReloadButton: false,
swfTakeover: true,
+ customConfig: "",
};
// TODO: Once https://crbug.com/798169 is addressed, just use browser.
@@ -106,6 +107,17 @@ export async function getExplicitOptions(): Promise {
delete options["responseHeadersUnsupported"];
}
+ // Handle customConfig JSON
+ if (options.customConfig) {
+ try {
+ const extra = JSON.parse(options.customConfig);
+ Object.assign(options, extra);
+ } catch (e) {
+ console.warn("Invalid custom_config JSON:", e);
+ }
+ delete options.customConfig;
+ }
+
return options;
}