rspace-online/lib/folk-piano.ts

291 lines
6.7 KiB
TypeScript

import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
const PIANO_URL = "https://musiclab.chromeexperiments.com/Shared-Piano/";
const styles = css`
:host {
background: #1e1e2e;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
min-width: 400px;
min-height: 300px;
overflow: hidden;
}
.piano-container {
width: 100%;
height: 100%;
position: relative;
}
.piano-iframe {
width: 100%;
height: 100%;
border: none;
}
.loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d44 100%);
color: white;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 18px;
gap: 12px;
}
.loading.hidden {
display: none;
}
.error {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d44 100%);
color: white;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
gap: 12px;
}
.error.hidden {
display: none;
}
.error-message {
font-size: 14px;
color: #f87171;
}
.retry-btn {
background: #6366f1;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
}
.retry-btn:hover {
background: #4f46e5;
}
.controls {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 4px;
}
.control-btn {
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 14px;
}
.control-btn:hover {
background: rgba(0, 0, 0, 0.7);
}
.minimized {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d44 100%);
color: white;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 24px;
cursor: pointer;
}
.minimized.hidden {
display: none;
}
`;
declare global {
interface HTMLElementTagNameMap {
"folk-piano": FolkPiano;
}
}
export class FolkPiano extends FolkShape {
static override tagName = "folk-piano";
static {
const sheet = new CSSStyleSheet();
const parentRules = Array.from(FolkShape.styles.cssRules)
.map((r) => r.cssText)
.join("\n");
const childRules = Array.from(styles.cssRules)
.map((r) => r.cssText)
.join("\n");
sheet.replaceSync(`${parentRules}\n${childRules}`);
this.styles = sheet;
}
#isMinimized = false;
#isLoading = true;
#hasError = false;
#iframe: HTMLIFrameElement | null = null;
#loadingEl: HTMLElement | null = null;
#errorEl: HTMLElement | null = null;
#minimizedEl: HTMLElement | null = null;
#containerEl: HTMLElement | null = null;
get isMinimized() {
return this.#isMinimized;
}
set isMinimized(value: boolean) {
this.#isMinimized = value;
this.#updateVisibility();
this.requestUpdate("isMinimized");
}
override createRenderRoot() {
const root = super.createRenderRoot();
const wrapper = document.createElement("div");
wrapper.innerHTML = html`
<div class="piano-container" data-drag>
<div class="loading">
<span>\u{1F3B9}</span>
<span>Loading Shared Piano...</span>
</div>
<div class="error hidden">
<span>\u{1F3B9}</span>
<span class="error-message">Failed to load piano</span>
<button class="retry-btn">Retry</button>
</div>
<div class="minimized hidden">
<span>\u{1F3B9} Shared Piano</span>
</div>
<iframe
class="piano-iframe"
src="${PIANO_URL}"
allow="microphone; camera; midi; autoplay; encrypted-media; fullscreen"
sandbox="allow-scripts allow-forms allow-popups allow-modals allow-same-origin"
style="opacity: 0;"
></iframe>
<div class="controls">
<button class="control-btn minimize-btn" title="Minimize">\u{1F53C}</button>
</div>
</div>
`;
// Replace the container div (slot's parent) with our piano container
const slot = root.querySelector("slot");
const containerDiv = slot?.parentElement as HTMLElement;
const pianoContainer = wrapper.querySelector(".piano-container");
if (containerDiv && pianoContainer) {
containerDiv.replaceWith(pianoContainer);
}
// Get references
this.#containerEl = root.querySelector(".piano-container");
this.#loadingEl = root.querySelector(".loading");
this.#errorEl = root.querySelector(".error");
this.#minimizedEl = root.querySelector(".minimized");
this.#iframe = root.querySelector(".piano-iframe");
const minimizeBtn = root.querySelector(".minimize-btn") as HTMLButtonElement;
const retryBtn = root.querySelector(".retry-btn") as HTMLButtonElement;
// Iframe load handling
this.#iframe?.addEventListener("load", () => {
this.#isLoading = false;
this.#hasError = false;
if (this.#loadingEl) this.#loadingEl.classList.add("hidden");
if (this.#iframe) this.#iframe.style.opacity = "1";
});
this.#iframe?.addEventListener("error", () => {
this.#isLoading = false;
this.#hasError = true;
if (this.#loadingEl) this.#loadingEl.classList.add("hidden");
if (this.#errorEl) this.#errorEl.classList.remove("hidden");
});
// Minimize toggle
minimizeBtn?.addEventListener("click", (e) => {
e.stopPropagation();
this.isMinimized = !this.#isMinimized;
minimizeBtn.textContent = this.#isMinimized ? "\u{1F53D}" : "\u{1F53C}";
});
// Click minimized view to expand
this.#minimizedEl?.addEventListener("click", (e) => {
e.stopPropagation();
this.isMinimized = false;
if (minimizeBtn) minimizeBtn.textContent = "\u{1F53C}";
});
// Retry button
retryBtn?.addEventListener("click", (e) => {
e.stopPropagation();
this.#retry();
});
// Suppress Chrome Music Lab console errors
window.addEventListener("error", (e) => {
if (e.message?.includes("musiclab") || e.filename?.includes("musiclab")) {
e.preventDefault();
}
});
return root;
}
#updateVisibility() {
if (!this.#iframe || !this.#minimizedEl) return;
if (this.#isMinimized) {
this.#iframe.style.display = "none";
this.#minimizedEl.classList.remove("hidden");
} else {
this.#iframe.style.display = "block";
this.#minimizedEl.classList.add("hidden");
}
}
#retry() {
if (!this.#iframe || !this.#errorEl || !this.#loadingEl) return;
this.#hasError = false;
this.#isLoading = true;
this.#errorEl.classList.add("hidden");
this.#loadingEl.classList.remove("hidden");
this.#iframe.style.opacity = "0";
this.#iframe.src = PIANO_URL;
}
override toJSON() {
return {
...super.toJSON(),
type: "folk-piano",
isMinimized: this.isMinimized,
};
}
}