291 lines
6.7 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|