Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

web: Core UI improvements #16793

Merged
merged 12 commits into from
Jun 30, 2024
175 changes: 104 additions & 71 deletions web/packages/core/src/ruffle-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export class RufflePlayer extends HTMLElement {
this.addVolumeControlsJavaScript(this.volumeControls);

const backupSaves = <HTMLElement>(
this.saveManager.querySelector("#backup-saves")
this.saveManager.querySelector(".modal-button")
);
if (backupSaves) {
backupSaves.addEventListener("click", this.backupSaves.bind(this));
Expand Down Expand Up @@ -355,12 +355,14 @@ export class RufflePlayer extends HTMLElement {
*/
private addModalJavaScript(modalElement: HTMLDivElement): void {
const videoHolder = modalElement.querySelector("#video-holder");
this.container.addEventListener("click", () => {
const hideModal = () => {
modalElement.classList.add("hidden");
if (videoHolder) {
videoHolder.textContent = "";
}
});
};

modalElement.parentNode!.addEventListener("click", hideModal);
const modalArea = modalElement.querySelector(".modal-area");
if (modalArea) {
modalArea.addEventListener("click", (event) =>
Expand All @@ -369,12 +371,7 @@ export class RufflePlayer extends HTMLElement {
}
const closeModal = modalElement.querySelector(".close-modal");
if (closeModal) {
closeModal.addEventListener("click", () => {
modalElement.classList.add("hidden");
if (videoHolder) {
videoHolder.textContent = "";
}
});
closeModal.addEventListener("click", hideModal);
}
}

Expand All @@ -387,55 +384,64 @@ export class RufflePlayer extends HTMLElement {
private addVolumeControlsJavaScript(
volumeControlsModal: HTMLDivElement,
): void {
const muteCheckbox = volumeControlsModal.querySelector(
const volumeMuteCheckbox = volumeControlsModal.querySelector(
"#mute-checkbox",
) as HTMLInputElement;
const volumeMuteIcon = volumeControlsModal.querySelector(
"#volume-mute",
) as HTMLLabelElement;
const volumeIcons = [
volumeControlsModal.querySelector(
"#volume-min",
) as HTMLLabelElement,
volumeControlsModal.querySelector(
"#volume-mid",
) as HTMLLabelElement,
volumeControlsModal.querySelector(
"#volume-max",
) as HTMLLabelElement,
];
const volumeSlider = volumeControlsModal.querySelector(
"#volume-slider",
) as HTMLInputElement;
const volumeSliderText = volumeControlsModal.querySelector(
"#volume-slider-text",
) as HTMLSpanElement;

const heading = volumeControlsModal.querySelector(
"#volume-controls-heading",
) as HTMLHeadingElement;
const muteCheckboxLabel = volumeControlsModal.querySelector(
"#mute-checkbox-label",
) as HTMLLabelElement;
const volumeSliderLabel = volumeControlsModal.querySelector(
"#volume-slider-label",
) as HTMLLabelElement;

// Add the texts.
heading.textContent = text("volume-controls");
muteCheckboxLabel.textContent = text("volume-controls-mute");
volumeSliderLabel.textContent = text("volume-controls-volume");
const setVolumeIcon = () => {
if (this.volumeSettings.isMuted) {
volumeMuteIcon.style.display = "inline";
volumeIcons.forEach((icon) => {
icon.style.display = "none";
});
} else {
volumeMuteIcon.style.display = "none";
const iconIndex = Math.round(this.volumeSettings.volume / 50);
volumeIcons.forEach((icon, i) => {
icon.style.display = i === iconIndex ? "inline" : "none";
});
}
};

// Set the controls to the current settings.
muteCheckbox.checked = this.volumeSettings.isMuted;
volumeSlider.disabled = muteCheckbox.checked;
volumeMuteCheckbox.checked = this.volumeSettings.isMuted;
volumeSlider.disabled = volumeMuteCheckbox.checked;
volumeSlider.valueAsNumber = this.volumeSettings.volume;
volumeSliderLabel.style.color = muteCheckbox.checked ? "grey" : "black";
volumeSliderText.style.color = muteCheckbox.checked ? "grey" : "black";
volumeSliderText.textContent = String(this.volumeSettings.volume);
volumeSliderText.textContent = volumeSlider.value + "%";
setVolumeIcon();

// Add event listeners to update the settings and controls.
muteCheckbox.addEventListener("change", () => {
volumeSlider.disabled = muteCheckbox.checked;
volumeSliderLabel.style.color = muteCheckbox.checked
? "grey"
: "black";
volumeSliderText.style.color = muteCheckbox.checked
? "grey"
: "black";
this.volumeSettings.isMuted = muteCheckbox.checked;
volumeMuteCheckbox.addEventListener("change", () => {
volumeSlider.disabled = volumeMuteCheckbox.checked;
this.volumeSettings.isMuted = volumeMuteCheckbox.checked;
this.instance?.set_volume(this.volumeSettings.get_volume());
setVolumeIcon();
});
volumeSlider.addEventListener("input", () => {
volumeSliderText.textContent = volumeSlider.value;
volumeSliderText.textContent = volumeSlider.value + "%";
this.volumeSettings.volume = volumeSlider.valueAsNumber;
this.instance?.set_volume(this.volumeSettings.get_volume());
setVolumeIcon();
});
}

Expand Down Expand Up @@ -1265,6 +1271,29 @@ export class RufflePlayer extends HTMLElement {
}
}

/**
* Check if there are any saves.
*
* @returns True if there is at least one save.
*/
private checkSaves(): boolean {
if (!this.saveManager.querySelector("#local-saves")) {
return false;
}
try {
if (localStorage === null) {
return false;
}
} catch (e: unknown) {
return false;
}
return Object.keys(localStorage).some((key) => {
const solName = key.split("/").pop();
const solData = localStorage.getItem(key);
return solName && solData && this.isB64SOL(solData);
});
}

/**
* Delete local save.
*
Expand All @@ -1281,17 +1310,10 @@ export class RufflePlayer extends HTMLElement {
* Puts the local save SOL file keys in a table.
*/
private populateSaves(): void {
const saveTable = this.saveManager.querySelector("#local-saves");
if (!saveTable) {
return;
}
try {
if (localStorage === null) {
return;
}
} catch (e: unknown) {
if (!this.checkSaves()) {
return;
}
const saveTable = this.saveManager.querySelector("#local-saves")!;
saveTable.textContent = "";
Object.keys(localStorage).forEach((key) => {
const solName = key.split("/").pop();
Expand All @@ -1303,8 +1325,9 @@ export class RufflePlayer extends HTMLElement {
keyCol.title = key;
const downloadCol = document.createElement("TD");
const downloadSpan = document.createElement("SPAN");
downloadSpan.textContent = text("save-download");
downloadSpan.className = "save-option";
downloadSpan.id = "download-save";
downloadSpan.title = text("save-download");
downloadSpan.addEventListener("click", () => {
const blob = this.base64ToBlob(
solData,
Expand All @@ -1325,17 +1348,19 @@ export class RufflePlayer extends HTMLElement {
document.createElement("LABEL")
);
replaceLabel.htmlFor = "replace-save-" + key;
replaceLabel.textContent = text("save-replace");
replaceLabel.className = "save-option";
replaceLabel.id = "replace-save";
replaceLabel.title = text("save-replace");
replaceInput.addEventListener("change", (event) =>
this.replaceSOL(event, key),
);
replaceCol.appendChild(replaceInput);
replaceCol.appendChild(replaceLabel);
const deleteCol = document.createElement("TD");
const deleteSpan = document.createElement("SPAN");
deleteSpan.textContent = text("save-delete");
deleteSpan.className = "save-option";
deleteSpan.id = "delete-save";
deleteSpan.title = text("save-delete");
deleteSpan.addEventListener("click", () =>
this.deleteSave(key),
);
Expand Down Expand Up @@ -1385,6 +1410,7 @@ export class RufflePlayer extends HTMLElement {
* Opens the save manager.
*/
private async openSaveManager(): Promise<void> {
this.populateSaves();
this.saveManager.classList.remove("hidden");
}

Expand Down Expand Up @@ -1521,9 +1547,8 @@ export class RufflePlayer extends HTMLElement {
navigator.clipboard.writeText(this.getPanicData()),
});
}
this.populateSaves();
const localSaveTable = this.saveManager.querySelector("#local-saves");
if (localSaveTable && localSaveTable.textContent !== "") {

if (this.checkSaves()) {
items.push({
text: text("context-menu-open-save-manager"),
onClick: this.openSaveManager.bind(this),
Expand Down Expand Up @@ -1605,15 +1630,16 @@ export class RufflePlayer extends HTMLElement {
}

private showContextMenu(event: MouseEvent | PointerEvent): void {
const modalOpen = Array.from(
this.shadow.querySelectorAll(".modal"),
).some((modal) => !modal.classList.contains("hidden"));
if (this.panicked || modalOpen) {
if (this.panicked) {
return;
}

event.preventDefault();

if (this.shadow.querySelectorAll(".modal:not(.hidden)").length !== 0) {
return;
}

if (event.type === "contextmenu") {
this.contextMenuSupported = true;
document.documentElement.addEventListener(
Expand Down Expand Up @@ -1687,22 +1713,29 @@ export class RufflePlayer extends HTMLElement {
}
}

// Place a context menu in the top-left corner, so
// its `clientWidth` and `clientHeight` are not clamped.
this.contextMenuElement.style.left = "0";
this.contextMenuElement.style.top = "0";
this.contextMenuOverlay.classList.remove("hidden");

const rect = this.getBoundingClientRect();
const x = event.clientX - rect.x;
const y = event.clientY - rect.y;
const maxX = rect.width - this.contextMenuElement.clientWidth - 1;
const maxY = rect.height - this.contextMenuElement.clientHeight - 1;
const playerRect = this.getBoundingClientRect();
const contextMenuRect = this.contextMenuElement.getBoundingClientRect();

// Keep the entire context menu inside the viewport.
// TODO: Allow the context menu to escape the document body while being mindful of scrollbars.
const overflowX = Math.max(
0,
event.clientX +
contextMenuRect.width -
document.documentElement.clientWidth,
);
const overflowY = Math.max(
0,
event.clientY +
contextMenuRect.height -
document.documentElement.clientHeight,
);
const x = event.clientX - playerRect.x - overflowX;
const y = event.clientY - playerRect.y - overflowY;

this.contextMenuElement.style.left =
Math.floor(Math.min(x, maxX)) + "px";
this.contextMenuElement.style.top =
Math.floor(Math.min(y, maxY)) + "px";
this.contextMenuElement.style.transform = `translate(${x}px, ${y}px)`;
}

private hideContextMenu(): void {
Expand Down
Loading