A music player that connects to your cloud/distributed storage.
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 & 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}