A music player that connects to your cloud/distributed storage. diffuse.sh
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307import { html, render } from "lit-html";import * as URI from "fast-uri";
import { facetFromURI } from "~/common/facets/utils.js";import facetsData from "~/_data/facets.json" with { type: "json" };import { loadAtProtoRecord } from "~/common/loader.js";import { saveFacet } from "./crud.js";
////////////////////////////////////////////// DIALOG////////////////////////////////////////////
// Track whether the user has manually edited the name/kind/description// fields, so the auto-fill triggered by URI changes doesn't stomp on their// input.let nameEdited = false;let kindEdited = false;let descriptionEdited = false;
/** * Best-effort auto-fill of the name, kind and description fields based on the * entered URI. * * - `diffuse://` URIs are looked up in the bundled facet catalogue (offline). * - `at://` URIs resolve the stored record; `name`, `kind` and `description` * are taken from the record when present. * - `https://` URIs use the document's `<title>` and meta description, falling * back to a name derived from the URL path when the document can't be * fetched. * * Fields the user has already edited are left alone. Failures are silent. * * @param {HTMLInputElement | null} uriEl * @param {HTMLInputElement | null} nameEl * @param {HTMLSelectElement | null} kindEl * @param {HTMLTextAreaElement | null} descriptionEl */async function inferFromURI(uriEl, nameEl, kindEl, descriptionEl) { const uri = uriEl?.value.trim(); if (!uri) return;
const scheme = URI.parse(uri).scheme;
if (scheme === "diffuse") { const entry = facetsData.find( (c) => c.url === uri.replace(/^diffuse:\/\//, ""), ); if (!entry) return; if (!nameEdited && nameEl && entry.title) nameEl.value = entry.title; if (!descriptionEdited && descriptionEl && entry.desc) { descriptionEl.value = entry.desc; } const kind = entry.kind === "prelude" ? "prelude" : "interactive"; if (!kindEdited && kindEl && kindEl.value !== kind) kindEl.value = kind; return; }
if (scheme === "at") { try { const value = await loadAtProtoRecord(uri); // A newer URI may have been entered while resolving — don't apply stale // results over it. if (uriEl?.value.trim() !== uri) return; if (!nameEdited && nameEl && typeof value.name === "string" && value.name) { nameEl.value = value.name; } if ( !descriptionEdited && descriptionEl && typeof value.description === "string" && value.description ) { descriptionEl.value = value.description; } if ( !kindEdited && kindEl && (value.kind === "interactive" || value.kind === "prelude") && kindEl.value !== value.kind ) { kindEl.value = value.kind; } } catch { // Unresolvable URI — leave the fields as-is. } return; }
if (scheme === "http" || scheme === "https") { const fallback = nameFromURL(uri); try { const res = await fetch(uri); if (!res.ok) throw new Error(`HTTP ${res.status}`); const doc = new DOMParser().parseFromString( await res.text(), "text/html", ); // A newer URI may have been entered while fetching — don't apply stale // results over it. if (uriEl?.value.trim() !== uri) return; const title = doc.querySelector("title")?.textContent?.trim(); if (title && !nameEdited && nameEl) nameEl.value = title; const meta = doc.querySelector('meta[name="description"]') ?.getAttribute("content") ?? doc.querySelector('meta[property="og:description"]') ?.getAttribute("content"); if (meta?.trim() && !descriptionEdited && descriptionEl) { descriptionEl.value = meta.trim(); } } catch { // Network/CORS failure — fall back to a name derived from the URL. if (!nameEdited && nameEl && fallback) nameEl.value = fallback; } }}
/** * Derives a display name from an HTTP(S) URL's path, used when the document * itself can't be fetched. E.g. `https://example.com/facets/foo-bar/index.html` * becomes `Foo Bar`. * * @param {string} url * @returns {string | undefined} */function nameFromURL(url) { try { const { pathname } = new URL(url); const segments = pathname.replace(/\/+$/, "").split("/").filter(Boolean); let last = segments.at(-1) ?? ""; if (last === "index.html" || last === "index.htm") { last = segments.at(-2) ?? ""; } const name = last .replace(/\.(html?|tile)$/, "") .split(/[-_]+/) .filter(Boolean) .map((word) => word[0].toUpperCase() + word.slice(1)) .join(" "); return name || undefined; } catch { return undefined; }}
export function openAddFromURIModal() { let dialog = /** @type {HTMLDialogElement | null} */ ( document.getElementById("add-from-uri-dialog") );
if (!dialog) { dialog = /** @type {HTMLDialogElement} */ ( document.createElement("dialog") );
dialog.id = "add-from-uri-dialog"; dialog.style.cssText = "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); margin: 0;";
render( html` <form id="add-from-uri-form"> <p style="font-size: var(--fs-sm)"> <strong>Load a facet from a URI.</strong> Currently supported URI schemes: <code>https</code>, <code>at</code> (AT Protocol) and <code>diffuse</code> (references internal facets). </p>
<div style="display: flex; flex-direction: column; gap: var(--space-xs)"> <div> <label>URI</label> <input id="add-uri-uri" type="text" placeholder="at://..., https://... or diffuse://..." required autocomplete="off" /> </div> <div> <label>Name</label> <input id="add-uri-name" type="text" placeholder="My Feature Name" required autocomplete="off" /> </div> <div> <label>Kind</label> <select id="add-uri-kind"> <option value="interactive">interface</option> <option value="prelude">feature</option> </select> </div> <div> <label>Description</label> <textarea id="add-uri-description" rows="3" placeholder="What does this facet do?" autocomplete="off" ></textarea> </div> </div> <div style="display: flex; font-size: var(--fs-sm); gap: var(--space-xs); margin-top: var(--space-sm)" > <button type="submit" class="button--bg-accent">Add</button> <button type="button" id="add-uri-cancel"> Cancel </button> </div> </form> `, dialog, );
document.body.appendChild(dialog);
const nameEl = /** @type {HTMLInputElement | null} */ ( dialog.querySelector("#add-uri-name") ); const kindEl = /** @type {HTMLSelectElement | null} */ ( dialog.querySelector("#add-uri-kind") ); const uriEl = /** @type {HTMLInputElement | null} */ ( dialog.querySelector("#add-uri-uri") ); const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( dialog.querySelector("#add-uri-description") );
nameEl?.addEventListener("input", () => { nameEdited = true; });
kindEl?.addEventListener("change", () => { kindEdited = true; });
descriptionEl?.addEventListener("input", () => { descriptionEdited = true; });
/** @type {ReturnType<typeof setTimeout> | undefined} */ let fillTimer; uriEl?.addEventListener("input", () => { clearTimeout(fillTimer); fillTimer = setTimeout(() => { void inferFromURI(uriEl, nameEl, kindEl, descriptionEl); }, 500); });
dialog.querySelector("#add-uri-cancel")?.addEventListener("click", () => { /** @type {HTMLDialogElement} */ (dialog).close(); });
dialog.querySelector("#add-from-uri-form")?.addEventListener( "submit", async (e) => { e.preventDefault();
const name = nameEl?.value.trim() ?? ""; const kind = kindEl?.value ?? "interactive"; const uri = uriEl?.value.trim() ?? ""; const description = descriptionEl?.value.trim() || undefined; if (!name || !uri) return;
const facet = await facetFromURI({ kind, name, uri, description }, { fetchHTML: false, });
await saveFacet(facet);
/** @type {HTMLDialogElement} */ (dialog).close(); }, ); }
const nameEl = /** @type {HTMLInputElement} */ ( dialog.querySelector("#add-uri-name") );
const kindEl = /** @type {HTMLSelectElement} */ ( dialog.querySelector("#add-uri-kind") );
const uriEl = /** @type {HTMLInputElement} */ ( dialog.querySelector("#add-uri-uri") );
const descriptionEl = /** @type {HTMLTextAreaElement} */ ( dialog.querySelector("#add-uri-description") );
if (nameEl) nameEl.value = ""; if (kindEl) kindEl.value = "interactive"; if (uriEl) uriEl.value = ""; if (descriptionEl) descriptionEl.value = "";
nameEdited = false; kindEdited = false; descriptionEdited = false;
dialog.showModal();}