A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a0924e204985facf3a9eb44f5f0f8a4e53e1ee83 293 lines 7.4 kB view raw
1import { basicSetup, EditorView } from "codemirror"; 2import { css as langCss } from "@codemirror/lang-css"; 3import { html as langHtml } from "@codemirror/lang-html"; 4import { javascript as langJs } from "@codemirror/lang-javascript"; 5import { autocompletion } from "@codemirror/autocomplete"; 6 7import * as TID from "@atcute/tid"; 8 9import * as CID from "~/common/cid.js"; 10import * as Output from "~/common/output.js"; 11import { facetFromURI } from "~/common/facets/utils.js"; 12import { loadURI } from "~/common/loader.js"; 13import { signal } from "~/common/signal.js"; 14 15import { saveFacet } from "./crud.js"; 16import { output } from "./output.js"; 17 18/** 19 * @import {Facet} from "~/definitions/types.d.ts" 20 */ 21 22const $editor = signal(/** @type {EditorView | null} */ (null)); 23const $editingFacet = signal(/** @type {Facet | null} */ (null)); 24 25//////////////////////////////////////////// 26// LOADING 27//////////////////////////////////////////// 28 29const LOADING_EL_ID = "editor-loading"; 30 31/** 32 * @param {boolean} loading 33 */ 34function setEditorLoading(loading) { 35 const container = /** @type {HTMLElement | null} */ ( 36 document.querySelector("#html-input-container") 37 ); 38 if (!container) return; 39 40 if (loading) { 41 if (document.getElementById(LOADING_EL_ID)) return; 42 const el = document.createElement("div"); 43 el.id = LOADING_EL_ID; 44 el.className = "with-icon"; 45 el.style.fontSize = "var(--fs-sm)"; 46 el.innerHTML = '<i class="ph-bold ph-spinner animate-spin"></i> Loading…'; 47 container.before(el); 48 container.hidden = true; 49 } else { 50 document.getElementById(LOADING_EL_ID)?.remove(); 51 container.hidden = false; 52 } 53} 54 55//////////////////////////////////////////// 56// EDITOR 57//////////////////////////////////////////// 58 59export function renderEditor() { 60 // Code editor 61 const editorContainer = document.body.querySelector("#html-input-container"); 62 if (!editorContainer) throw new Error("Editor container not found"); 63 64 const editor = new EditorView({ 65 parent: editorContainer, 66 doc: ` 67<style> 68 @import "./styles/base.css"; 69</style> 70 71<script type="module"> 72 import foundation from "~/common/foundation.js"; 73</script> 74 `.trim(), 75 extensions: [ 76 basicSetup, 77 langHtml(), 78 langCss(), 79 langJs(), 80 autocompletion(), 81 ], 82 }); 83 84 $editor.value = editor; 85 return editor; 86} 87 88//////////////////////////////////////////// 89// FORM 90//////////////////////////////////////////// 91 92/** 93 * @param {EditorView} editor 94 */ 95const onBuildSubmit = (editor) => 96/** 97 * @param {Event} event 98 */ 99async (event) => { 100 event.preventDefault(); 101 102 const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 103 "#name-input", 104 )); 105 106 const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( 107 document.querySelector("#description-input") 108 ); 109 110 const kindEl = /** @type {HTMLSelectElement | null} */ ( 111 document.querySelector("#kind-input") 112 ); 113 114 const html = editor.state.doc.toString(); 115 const cid = await CID.create(0x55, new TextEncoder().encode(html)); 116 const name = nameEl?.value ?? "nameless"; 117 const description = descriptionEl?.value ?? ""; 118 const kind = 119 /** @type {"interactive" | "prelude"} */ (kindEl?.value ?? "interactive"); 120 121 /** @type {Facet} */ 122 const facet = $editingFacet.value 123 ? { 124 ...$editingFacet.value, 125 cid, 126 description, 127 html, 128 kind, 129 name, 130 } 131 : { 132 $type: "sh.diffuse.output.facet", 133 id: TID.now(), 134 cid, 135 description, 136 html, 137 kind, 138 name, 139 }; 140 141 switch (/** @type {any} */ (event).submitter.name) { 142 case "save": 143 await saveFacet(facet); 144 break; 145 case "save+open": 146 await saveFacet(facet); 147 globalThis.open(`./l/?id=${facet.id}`, "blank"); 148 break; 149 } 150}; 151 152/** 153 * @param {Facet} ogFacet 154 */ 155async function editFacet(ogFacet) { 156 const facet = { ...ogFacet }; 157 const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 158 "#name-input", 159 )); 160 161 const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( 162 document.querySelector("#description-input") 163 ); 164 165 const kindEl = /** @type {HTMLSelectElement | null} */ ( 166 document.querySelector("#kind-input") 167 ); 168 169 if (!nameEl) return; 170 171 // Reset url — remove `id` param if not matching the facet 172 const url = new URL(location.href); 173 const id = url.searchParams.get("id"); 174 175 if (id && facet.id !== id) { 176 url.searchParams.delete("id"); 177 history.replaceState(null, "", url); 178 } 179 180 // Scroll to builder 181 document.querySelector("#code")?.scrollIntoView(); 182 183 // Make sure HTML is loaded 184 if (!facet.html && facet.uri) { 185 setEditorLoading(true); 186 const html = await loadURI(facet.uri); 187 const cid = await CID.create(0x55, new TextEncoder().encode(html)); 188 setEditorLoading(false); 189 190 facet.html = html; 191 facet.cid = cid; 192 } 193 194 $editingFacet.value = facet; 195 nameEl.value = facet.name; 196 197 if (kindEl) { 198 kindEl.value = facet.kind ?? "interactive"; 199 } 200 201 if (descriptionEl) { 202 descriptionEl.value = facet.description ?? ""; 203 } 204 205 const editor = $editor.value; 206 editor?.dispatch({ 207 changes: { from: 0, to: editor.state.doc.length, insert: facet.html }, 208 }); 209} 210 211export function handleBuildFormSubmit() { 212 const editor = $editor.value; 213 if (!editor) return; 214 215 document.querySelector("#code-form")?.addEventListener( 216 "submit", 217 onBuildSubmit(editor), 218 ); 219} 220 221//////////////////////////////////////////// 222// EDIT EXAMPLES 223//////////////////////////////////////////// 224 225let isListening = false; 226 227export function listenForExamplesEdit() { 228 if (isListening) return; 229 isListening = true; 230 231 document.body.addEventListener( 232 "click", 233 /** 234 * @param {MouseEvent} event 235 */ 236 async (event) => { 237 const target = /** @type {HTMLElement} */ (event.target); 238 const rel = target.getAttribute("rel"); 239 if (!rel) return; 240 241 const uri = target.closest("li")?.getAttribute("data-uri"); 242 if (!uri) return; 243 244 const name = target.closest("li")?.getAttribute("data-name"); 245 if (!name) return; 246 247 const kind = target.closest("li")?.getAttribute("data-kind") ?? undefined; 248 249 switch (rel) { 250 case "edit": { 251 setEditorLoading(true); 252 const facet = await facetFromURI({ kind, name, uri }, { 253 fetchHTML: true, 254 }); 255 setEditorLoading(false); 256 editFacet(facet); 257 document.querySelector("#code")?.scrollIntoView(); 258 break; 259 } 260 } 261 }, 262 ); 263} 264 265//////////////////////////////////////////// 266// EDIT FACET FROM URL 267//////////////////////////////////////////// 268 269export async function editFacetFromURL() { 270 const params = new URLSearchParams(location.search); 271 const idParam = params.get("id"); 272 const uriParam = params.get("uri"); 273 274 setEditorLoading(true); 275 try { 276 if (idParam) { 277 const out = await output(); 278 const col = await Output.data(out.facets); 279 const facet = col.find((f) => f.id === idParam); 280 if (facet) await editFacet(facet); 281 } else if (uriParam) { 282 const facet = await facetFromURI({ 283 uri: uriParam, 284 name: params.get("name") ?? "", 285 kind: /** @type {any} */ (params.get("kind") ?? undefined), 286 description: params.get("description") ?? undefined, 287 }, { fetchHTML: true }); 288 await editFacet(facet); 289 } 290 } finally { 291 setEditorLoading(false); 292 } 293}