A music player that connects to your cloud/distributed storage.
at v4 221 lines 5.6 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 foundation from "~/common/facets/foundation.js"; 12import { facetFromURI } from "~/common/facets/utils.js"; 13import { loadURI } from "~/common/loader.js"; 14import { signal } from "~/common/signal.js"; 15 16/** 17 * @import {Facet} from "~/definitions/types.d.ts" 18 */ 19 20//////////////////////////////////////////// 21// BUILD 22//////////////////////////////////////////// 23 24const output = foundation.orchestrator.output(); 25const $editingFacet = signal(/** @type {Facet | null} */ (null)); 26 27// Code editor 28const editorContainer = document.body.querySelector("#html-input-container"); 29if (!editorContainer) throw new Error("Editor container not found"); 30 31const editor = new EditorView({ 32 parent: editorContainer, 33 doc: ` 34<main> 35 <h1 id="now-playing"> 36 Waiting on tracks &amp; queue to load ... 37 </h1> 38</main> 39 40<style> 41 @import "./styles/base.css"; 42 @import "./styles/diffuse/page.css"; 43</style> 44 45<script type="module"> 46 import foundation from "~/common/facets/foundation.js"; 47 import { effect } from "~/common/signal.js"; 48 49 const components = foundation.features.fillQueueAutomatically(); 50 const myHtmlElement = document.querySelector("#now-playing"); 51 52 effect(() => { 53 const now = components.engine.queue.now(); 54 const currentlyPlaying = now ? components.orchestrator.output.tracks.collection().find(t => t.id === now.id) : undefined; 55 if (currentlyPlaying && myHtmlElement) { 56 myHtmlElement.innerText = \`\$\{currentlyPlaying.tags.artist} - \$\{currentlyPlaying.tags.title}\`; 57 } 58 }) 59</script> 60 `.trim(), 61 extensions: [ 62 basicSetup, 63 langHtml(), 64 langCss(), 65 langJs(), 66 autocompletion(), 67 ], 68}); 69 70// Form submit 71document.querySelector("#build-form")?.addEventListener( 72 "submit", 73 onBuildSubmit, 74); 75 76/** 77 * @param {Event} event 78 */ 79async function onBuildSubmit(event) { 80 event.preventDefault(); 81 82 const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 83 "#name-input", 84 )); 85 86 const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( 87 document.querySelector("#description-input") 88 ); 89 90 const html = editor.state.doc.toString(); 91 const cid = await CID.create(0x55, new TextEncoder().encode(html)); 92 const name = nameEl?.value ?? "nameless"; 93 const description = descriptionEl?.value ?? ""; 94 95 /** @type {Facet} */ 96 const facet = $editingFacet.value 97 ? { 98 ...$editingFacet.value, 99 cid, 100 description, 101 html, 102 name, 103 } 104 : { 105 $type: "sh.diffuse.output.facet", 106 id: TID.now(), 107 cid, 108 description, 109 html, 110 name, 111 }; 112 113 switch (/** @type {any} */ (event).submitter.name) { 114 case "save": 115 await saveFacet(facet); 116 break; 117 case "save+open": 118 await saveFacet(facet); 119 globalThis.open(`./facets/l/?id=${facet.id}`, "blank"); 120 break; 121 } 122} 123 124/** 125 * @param {Facet} ogFacet 126 */ 127async function editFacet(ogFacet) { 128 const facet = { ...ogFacet }; 129 const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 130 "#name-input", 131 )); 132 133 const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( 134 document.querySelector("#description-input") 135 ); 136 137 if (!nameEl) return; 138 139 // Scroll to builder 140 document.querySelector("#build")?.scrollIntoView(); 141 142 // Make sure HTML is loaded 143 if (!facet.html && facet.uri) { 144 const html = await loadURI(facet.uri); 145 const cid = await CID.create(0x55, new TextEncoder().encode(html)); 146 147 facet.html = html; 148 facet.cid = cid; 149 } 150 151 $editingFacet.value = facet; 152 nameEl.value = facet.name; 153 154 if (descriptionEl) { 155 descriptionEl.value = facet.description ?? ""; 156 } 157 158 editor.dispatch({ 159 changes: { from: 0, to: editor.state.doc.length, insert: facet.html }, 160 }); 161} 162 163/** 164 * @param {Facet} facet 165 */ 166 167async function saveFacet(facet) { 168 await Output.waitUntilLoaded(output.facets); 169 170 const col = output.facets.collection(); 171 const colWithoutId = col.filter((c) => c.id !== facet.id); 172 await output.facets.save([...colWithoutId, { 173 ...facet, 174 updatedAt: new Date().toISOString(), 175 }]); 176} 177 178//////////////////////////////////////////// 179// SAVE & FORK 180//////////////////////////////////////////// 181 182document.body.addEventListener( 183 "click", 184 /** 185 * @param {MouseEvent} event 186 */ 187 async (event) => { 188 const target = /** @type {HTMLElement} */ (event.target); 189 const rel = target.getAttribute("rel"); 190 if (!rel) return; 191 192 const uri = target.closest("li")?.getAttribute("data-uri"); 193 if (!uri) return; 194 195 const name = target.closest("li")?.getAttribute("data-name"); 196 if (!name) return; 197 198 switch (rel) { 199 case "edit": { 200 const facet = await facetFromURI({ name, uri }, { fetchHTML: true }); 201 editFacet(facet); 202 document.querySelector("#build")?.scrollIntoView(); 203 break; 204 } 205 } 206 }, 207); 208 209//////////////////////////////////////////// 210// 🚀 211//////////////////////////////////////////// 212 213await Output.waitUntilLoaded(output.facets); 214 215// Load facet from url 216const idParam = new URLSearchParams(location.search).get("id"); 217 218if (idParam) { 219 const facet = output.facets.collection().find((f) => f.id === idParam); 220 if (facet) await editFacet(facet); 221}