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

Configure Feed

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

at 1680a1e4e4b5ca50148f808c16fa15cbee2cdeb0 236 lines 7.7 kB view raw
1import { html, nothing, render as litRender } from "lit-html"; 2 3/** 4 * @import { TemplateResult } from "lit-html" 5 */ 6 7/** 8 * @typedef {{ name: string; detail: string; isInput: boolean; isOutput: boolean; isSelectedOutput: boolean; onRemove: () => void }} ConnectItem 9 */ 10 11/** 12 * Sets up a connect facet UI: a card with "Add audio input" and 13 * "Use as userdata storage" buttons, a dialog with a form, and a 14 * reactive list of configured items below a divider. 15 * 16 * @param {Object} config 17 * @param {string} config.title - Card header title 18 * @param {TemplateResult | string} config.description - Content shown on the left side 19 * @param {TemplateResult} [config.rightContent] - Extra content shown at the top of the right side 20 * @param {TemplateResult} config.formFields - Form body content (inputs, footnotes, etc.) 21 * @param {(mode: 'input' | 'output') => Promise<void>} config.onSubmit 22 * @param {boolean} [config.hasInput] - Whether to show the "Add audio input" button (default: true) 23 * @param {boolean} [config.hasOutput] - Whether to show the "Use as userdata storage" button (default: true) 24 * @param {() => Promise<void>} [config.onOutputActivate] - Called instead of opening the dialog when output is already configured but inactive 25 * 26 * @returns {{ setItems: (items: ConnectItem[]) => void, setError: (message: string | null) => void }} 27 */ 28export function setup( 29 { 30 title, 31 description, 32 rightContent = nothing, 33 formFields, 34 onSubmit, 35 hasInput = true, 36 hasOutput = true, 37 onOutputActivate, 38 }, 39) { 40 const main = document.querySelector("main"); 41 if (!main) throw new Error("No <main> element"); 42 43 litRender( 44 html` 45 <div class="facet__left"> 46 <div> 47 <a href="./dashboard/" class="diffuse-logo-container"> 48 <svg viewBox="0 0 902 134" width="160"> 49 <title>Diffuse</title> 50 <use 51 xlink:href="images/diffuse-current.svg#diffuse" 52 href="images/diffuse-current.svg#diffuse" 53 ></use> 54 </svg> 55 </a> 56 </div> 57 <h1>${title}</h1> 58 ${description} 59 </div> 60 <div class="facet__right"> 61 ${rightContent} 62 <div class="button-row"> 63 ${hasInput 64 ? html` 65 <button id="connect-add-input-btn"> 66 <i class="ph-fill ph-music-notes"></i> 67 Add audio input 68 </button> 69 ` 70 : nothing} 71 ${hasOutput 72 ? html` 73 <button id="connect-add-output-btn" class="button--brand"> 74 <i class="ph-fill ph-person"></i> 75 Use as userdata storage 76 </button> 77 ` 78 : nothing} 79 </div> 80 <div id="connect-card-error" class="callout callout--danger" hidden></div> 81 <hr id="connect-divider" hidden> 82 <ul id="connect-list" class="connect-list" hidden></ul> 83 </div> 84 85 <dialog id="connect-dialog"> 86 <div class="dialog-header"> 87 <strong id="connect-dialog-title"></strong> 88 </div> 89 <form id="connect-form" class="dialog-body"> 90 ${formFields} 91 <div id="connect-error" class="callout callout--danger" hidden></div> 92 </form> 93 <div class="dialog-footer"> 94 <button id="connect-submit-btn" type="submit" form="connect-form" class="button--brand">Add</button> 95 <button id="connect-cancel-btn" type="button">Cancel</button> 96 </div> 97 </dialog> 98 `, 99 main, 100 ); 101 102 const dialog = 103 /** @type {HTMLDialogElement} */ (main.querySelector("#connect-dialog")); 104 const dialogTitleEl = 105 /** @type {HTMLElement} */ (main.querySelector("#connect-dialog-title")); 106 const form = 107 /** @type {HTMLFormElement} */ (main.querySelector("#connect-form")); 108 const dialogErrorEl = 109 /** @type {HTMLElement} */ (main.querySelector("#connect-error")); 110 const cardErrorEl = 111 /** @type {HTMLElement} */ (main.querySelector("#connect-card-error")); 112 const divider = 113 /** @type {HTMLElement} */ (main.querySelector("#connect-divider")); 114 const list = /** @type {HTMLElement} */ (main.querySelector("#connect-list")); 115 const outputBtn = 116 /** @type {HTMLElement} */ (main.querySelector("#connect-add-output-btn")); 117 118 /** @type {'input' | 'output'} */ 119 let mode = "input"; 120 121 /** @type {ConnectItem[]} */ 122 let currentItems = []; 123 124 /** @param {string | null} message */ 125 const setDialogError = (message) => { 126 dialogErrorEl.hidden = message === null; 127 dialogErrorEl.textContent = message; 128 }; 129 130 /** @param {string | null} message */ 131 const setError = (message) => { 132 cardErrorEl.hidden = message === null; 133 cardErrorEl.textContent = message; 134 }; 135 136 /** @param {'input' | 'output'} m */ 137 const openDialog = (m) => { 138 mode = m; 139 dialogTitleEl.textContent = m === "input" 140 ? "Add audio input" 141 : "Use as userdata storage"; 142 form.reset(); 143 setDialogError(null); 144 dialog.showModal(); 145 }; 146 147 if (hasInput) { 148 main 149 .querySelector("#connect-add-input-btn") 150 ?.addEventListener("click", () => openDialog("input")); 151 } 152 153 main 154 .querySelector("#connect-add-output-btn") 155 ?.addEventListener("click", () => { 156 if (onOutputActivate && currentItems.some((i) => i.isOutput)) { 157 onOutputActivate(); 158 } else { 159 openDialog("output"); 160 } 161 }); 162 163 main.querySelector("#connect-cancel-btn")?.addEventListener("click", () => { 164 setDialogError(null); 165 dialog.close(); 166 }); 167 168 const submitBtn = 169 /** @type {HTMLElement} */ (main.querySelector("#connect-submit-btn")); 170 171 form.addEventListener("submit", async (e) => { 172 e.preventDefault(); 173 setDialogError(null); 174 submitBtn.setAttribute("disabled", ""); 175 submitBtn.textContent = "Loading …"; 176 try { 177 await onSubmit(mode); 178 dialog.close(); 179 } catch (err) { 180 setDialogError( 181 err instanceof Error ? err.message : "Something went wrong", 182 ); 183 } finally { 184 submitBtn.removeAttribute("disabled"); 185 submitBtn.textContent = "Add"; 186 } 187 }); 188 189 return { 190 setError, 191 192 /** 193 * Updates the list of configured items below the divider. 194 * Call inside an effect() for reactivity. 195 * 196 * @param {ConnectItem[]} items 197 */ 198 setItems(items) { 199 currentItems = items; 200 divider.hidden = items.length === 0; 201 list.hidden = items.length === 0; 202 if (outputBtn) outputBtn.hidden = items.some((i) => i.isOutput && i.isSelectedOutput); 203 litRender( 204 html` 205 ${items.map( 206 ({ name, detail, isInput, isOutput, isSelectedOutput, onRemove }) => 207 html` 208 <li class="connect-item"> 209 <div class="connect-item__info"> 210 <span class="connect-item__name">${name}</span> 211 <span class="connect-item__detail">${detail}</span> 212 </div> 213 <div class="connect-item__tags"> 214 ${isInput 215 ? html`<span class="badge">Input</span>` 216 : nothing} 217 ${isOutput 218 ? html`<span class="badge ${isSelectedOutput ? "badge--brand" : "badge--warning"}">Output</span>` 219 : nothing} 220 </div> 221 <button 222 class="button--plain button--small" 223 aria-label="Remove" 224 @click="${onRemove}" 225 > 226 <i class="ph-bold ph-x"></i> 227 </button> 228 </li> 229 `, 230 )} 231 `, 232 list, 233 ); 234 }, 235 }; 236}