A music player that connects to your cloud/distributed storage. diffuse.sh
Something went wrong. Try again.
15 kB · 463 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464import { Temporal } from "~/common/temporal.js";
import "@awesome.me/webawesome/dist/components/split-panel/split-panel.js";import "@awesome.me/webawesome/dist/components/dialog/dialog.js";import "@awesome.me/webawesome/dist/components/button/button.js";import "@awesome.me/webawesome/dist/components/input/input.js";import "@awesome.me/webawesome/dist/components/icon/icon.js";import "@awesome.me/webawesome/dist/components/select/select.js";import "@awesome.me/webawesome/dist/components/option/option.js";
import "~/common/webawesome/detect-dark.js";import foundation from "~/common/foundation.js";import * as Output from "~/common/output.js";
// Set doc titlefoundation.setup({ title: "Split View | Diffuse" });
/** * @import { default as WaSplitPanel } from "@awesome.me/webawesome/dist/components/split-panel/split-panel.js" * @import { default as WaDialog } from "@awesome.me/webawesome/dist/components/dialog/dialog.js" * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" * @import { default as WaIcon } from "@awesome.me/webawesome/dist/components/icon/icon.js" * @import { default as WaSelect } from "@awesome.me/webawesome/dist/components/select/select.js" */
/** * @typedef {{ type: "pane", facet: string | null }} PaneNode * @typedef {{ type: "split", orientation: "horizontal" | "vertical", position: number, start: Node, end: Node }} SplitNode * @typedef {PaneNode | SplitNode} Node */
const STORAGE_KEY = "facets/misc/split-view/builder/layout";
// ─── State ───────────────────────────────────────────────────────────────────
/** @type {Node} */let state = loadState();
let editMode = false;
/** @type {string | null} */let pendingPaneId = null;
/** @returns {Node} */function loadState() { try { return /** @type {Node} */ (JSON.parse( localStorage.getItem(STORAGE_KEY) ?? "null", )) ?? defaultState(); } catch { return defaultState(); }}
function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state));}
/** @returns {PaneNode} */function defaultState() { return { type: "pane", facet: null };}
// ─── Tree helpers ─────────────────────────────────────────────────────────────// Nodes are identified by a dot-separated path: "root", "root.start", "root.end.start"
/** * @param {string} path * @returns {Node} */function getNode(path) { if (path === "root") return state; const parts = path.replace(/^root\./, "").split("."); /** @type {any} */ let node = state; for (const part of parts) node = node[part]; return /** @type {Node} */ (node);}
/** * @param {string} path * @param {Node} newNode */function replaceNode(path, newNode) { if (path === "root") { state = newNode; return; } const parts = path.split("."); const parentPath = parts.slice(0, -1).join("."); const childKey = parts[parts.length - 1]; const parent = /** @type {any} */ (getNode(parentPath)); parent[childKey] = newNode;}
// ─── Mutations ────────────────────────────────────────────────────────────────
/** * @param {string} nodePath * @param {"horizontal" | "vertical"} orientation */function splitPane(nodePath, orientation) { const existing = getNode(nodePath); replaceNode(nodePath, { type: "split", orientation, position: 50, start: existing, end: { type: "pane", facet: null }, }); saveState(); render();}
/** @param {string} nodePath */function removePane(nodePath) { if (nodePath === "root") return; const parts = nodePath.split("."); const parentPath = parts.slice(0, -1).join("."); const childKey = parts[parts.length - 1]; const siblingKey = childKey === "start" ? "end" : "start"; const parent = /** @type {any} */ (getNode(parentPath)); const sibling = /** @type {Node} */ (parent[siblingKey]); replaceNode(parentPath, sibling); saveState(); render();}
/** * @param {string} nodePath * @param {string} facetPath */function setFacet(nodePath, facetPath) { const pane = /** @type {PaneNode} */ (getNode(nodePath)); pane.facet = facetPath; saveState(); render();}
// ─── Rendering ────────────────────────────────────────────────────────────────
const layout = /** @type {HTMLElement} */ (document.querySelector("#layout"));
function render() { layout.innerHTML = ""; layout.appendChild(renderNode(state, "root", true)); layout.classList.toggle("edit-mode", editMode);}
/** * @param {Node} node * @param {string} path * @param {boolean} isRoot * @returns {HTMLElement} */function renderNode(node, path, isRoot) { if (node.type === "split") { const panel = /** @type {WaSplitPanel} */ (document.createElement("wa-split-panel")); panel.position = node.position; if (node.orientation === "vertical") panel.orientation = "vertical";
const startSlot = document.createElement("div"); startSlot.slot = "start"; startSlot.appendChild(renderNode(node.start, path + ".start", false));
const endSlot = document.createElement("div"); endSlot.slot = "end"; endSlot.appendChild(renderNode(node.end, path + ".end", false));
panel.appendChild(startSlot); panel.appendChild(endSlot);
panel.addEventListener("wa-reposition", () => { const splitNode = /** @type {SplitNode} */ (getNode(path)); splitNode.position = panel.position; saveState(); });
return panel; }
// Pane const pane = document.createElement("div"); pane.className = "pane" + (node.facet ? "" : " pane--empty");
if (node.facet) { const iframe = document.createElement("iframe"); const uri = node.facet.includes("://") ? node.facet : `diffuse://${node.facet}`; iframe.src = "l/?uri=" + encodeURIComponent(uri); iframe.allow = "autoplay"; pane.appendChild(iframe); }
const overlay = document.createElement("div"); overlay.className = "pane-overlay";
if (node.facet) { const title = document.querySelector(`#facet-select wa-option[value="${node.facet}"]`) ?.textContent?.trim() ?? node.facet; const label = document.createElement("div"); label.className = "pane-name"; label.style.fontWeight = "700"; label.textContent = title; overlay.appendChild(label); }
overlay.appendChild( makeWaButton( node.facet ? "Change facet" : "+ Add facet", "neutral", "filled", () => openPicker(path), ), ); overlay.appendChild( makeWaButton( "Split left / right", "neutral", "outlined", () => splitPane(path, "horizontal"), ), ); overlay.appendChild( makeWaButton( "Split top / bottom", "neutral", "outlined", () => splitPane(path, "vertical"), ), );
if (!isRoot) { overlay.appendChild( makeWaButton("Remove", "danger", "outlined", () => removePane(path)), ); }
pane.appendChild(overlay); return pane;}
/** * @param {string} text * @param {string} variant * @param {string} appearance * @param {() => void} onClick * @returns {HTMLElement} */function makeWaButton(text, variant, appearance, onClick) { const btn = document.createElement("wa-button"); btn.setAttribute("variant", variant); btn.setAttribute("appearance", appearance); btn.setAttribute("size", "small"); btn.style.width = "100%"; btn.textContent = text; btn.addEventListener("click", (e) => { e.stopPropagation(); onClick(); }); return btn;}
// ─── Divider drag: disable iframe pointer events while dragging ───────────────
document.addEventListener("mousedown", (e) => { const isDivider = e.composedPath().some( (el) => el instanceof Element && el.getAttribute("part") === "divider", ); if (isDivider) layout.classList.add("dragging");}, { capture: true });
document.addEventListener("mouseup", () => { layout.classList.remove("dragging");});
// ─── Edit mode ────────────────────────────────────────────────────────────────
const editToggle = /** @type {HTMLElement} */ (document.querySelector("#edit-toggle"));const editIcon = /** @type {WaIcon} */ (document.querySelector("#edit-icon"));const saveCopyBtn = /** @type {HTMLElement} */ (document.querySelector("#save-copy-btn"));const saveCopyIcon = /** @type {WaIcon} */ (document.querySelector("#save-copy-icon"));
editToggle.addEventListener("click", () => { editMode = !editMode; editToggle.setAttribute("aria-label", editMode ? "Done" : "Edit layout"); editIcon.name = editMode ? "xmark" : "border-all"; layout.classList.toggle("edit-mode", editMode); saveCopyBtn.style.display = editMode ? "" : "none";});
saveCopyBtn.addEventListener("click", async () => { await saveSimplifiedCopy(); saveCopyIcon.name = "check"; setTimeout(() => { saveCopyIcon.name = "floppy-disk"; }, 2000);});
// ─── Facet picker ─────────────────────────────────────────────────────────────
const pickerDialog = /** @type {WaDialog} */ (document.querySelector("#facet-picker"));const facetSelect = /** @type {WaSelect} */ (document.querySelector("#facet-select"));const customPath = /** @type {WaInput} */ (document.querySelector("#custom-path"));const customConfirm = /** @type {HTMLElement} */ (document.querySelector("#custom-confirm"));
customConfirm.addEventListener("click", () => { const val = customPath.value?.trim() || /** @type {string} */ (facetSelect.value) || ""; if (val && pendingPaneId !== null) setFacet(pendingPaneId, val); pendingPaneId = null; pickerDialog.open = false;});
pickerDialog.addEventListener("wa-hide", (e) => { if (e.target === pickerDialog) pendingPaneId = null;});
/** @param {string} nodePath */function openPicker(nodePath) { pendingPaneId = nodePath; facetSelect.value = null; customPath.value = ""; pickerDialog.open = true;}
// ─── Save simplified copy ─────────────────────────────────────────────────────
/** * @param {Node} node * @param {string} indent * @returns {string} */function generateNodeHTML(node, indent = "") { const inner = indent + " ";
if (node.type === "split") { const orientationAttr = node.orientation === "vertical" ? ' orientation="vertical"' : ""; return `${indent}<wa-split-panel position="${node.position}"${orientationAttr}>${inner}<div slot="start">${generateNodeHTML(node.start, inner + " ")}${inner}</div>${inner}<div slot="end">${generateNodeHTML(node.end, inner + " ")}${inner}</div>${indent}</wa-split-panel>`; }
if (node.facet) { const uri = node.facet.includes("://") ? node.facet : `diffuse://${node.facet}`; const src = "l/?uri=" + encodeURIComponent(uri); return `${indent}<div class="pane">${inner}<iframe src="${src}" allow="autoplay"></iframe>${indent}</div>`; }
return `${indent}<div class="pane"></div>`;}
/** * @param {string} id * @param {string} name */function generateSimplifiedHTML(id, name) { const scriptClose = "</" + "script>"; return `\<link rel="stylesheet" href="vendor/@awesome.me/webawesome/styles/themes/default.css" />
<style> body { margin: 0; height: 100dvh; overflow: hidden; } #layout, #layout > * { height: 100%; } wa-split-panel { height: 100%; } [slot="start"], [slot="end"] { height: 100%; } .pane { height: 100%; } .pane iframe { border: none; width: 100%; height: 100%; } .dragging iframe { pointer-events: none; }</style>
<div id="layout">${generateNodeHTML(state, " ")}</div>
<script type="module"> import "@awesome.me/webawesome/dist/components/split-panel/split-panel.js"; import "~/common/webawesome/detect-dark.js"; import foundation from "~/common/foundation.js";
foundation.setup({ title: ${JSON.stringify(name + " | Diffuse")} });
const layout = document.querySelector("#layout");
document.addEventListener("mousedown", (e) => { const isDivider = e.composedPath().some( (el) => el instanceof Element && el.getAttribute("part") === "divider", ); if (isDivider) layout.classList.add("dragging"); }, { capture: true });
document.addEventListener("mouseup", () => { layout.classList.remove("dragging"); });
const POSITIONS_KEY = "facets/misc/split-view/${id}/positions"; const savedPositions = (() => { try { return JSON.parse(localStorage.getItem(POSITIONS_KEY) ?? "{}"); } catch { return {}; } })();
document.querySelectorAll("wa-split-panel").forEach((panel, i) => { if (savedPositions[i] !== undefined) panel.position = savedPositions[i]; panel.addEventListener("wa-reposition", () => { savedPositions[i] = panel.position; localStorage.setItem(POSITIONS_KEY, JSON.stringify(savedPositions)); }); });
foundation.ready();${scriptClose}`;}
async function saveSimplifiedCopy() { const output = await foundation.orchestrator.output(); const id = crypto.randomUUID(); const name = `Split View (${Temporal.Now.instant().toLocaleString()})`; const html = generateSimplifiedHTML(id, name); const now = new Date().toISOString();
await output.facets.save([ ...(await Output.data(output.facets)), { $type: "sh.diffuse.output.facet", id, name, html, createdAt: now, updatedAt: now, }, ]);}
// ─── Init ─────────────────────────────────────────────────────────────────────
render();
////////////////////////////////////////////// 🚀////////////////////////////////////////////
foundation.ready();