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 v4 317 lines 9.8 kB view raw
1import { html, render } from "lit-html"; 2import { classMap } from "lit-html/directives/class-map.js"; 3import { keyed } from "lit-html/directives/keyed.js"; 4import { marked } from "marked"; 5import { unsafeHTML } from "lit-html/directives/unsafe-html.js"; 6 7import * as FacetCategory from "~/common/facets/category.js"; 8import { effect, signal } from "~/common/signal.js"; 9 10import { nothing } from "~/common/element.js"; 11 12import { deleteFacet, toggleFacetEnabled } from "./crud.js"; 13import { output } from "./output.js"; 14import { openAddFromURIModal } from "./from-uri.js"; 15 16// Signals 17const FILTER_STORAGE_KEY = "diffuse/dashboard/filter"; 18const storedFilter = localStorage.getItem(FILTER_STORAGE_KEY); 19const activeFilter = signal( 20 storedFilter === "prelude" || storedFilter === "interface" || 21 storedFilter === "base" 22 ? storedFilter 23 : "all", 24); 25 26effect(() => { 27 localStorage.setItem(FILTER_STORAGE_KEY, activeFilter.get()); 28}); 29 30/** 31 * @import OutputOrchestrator from "~/components/orchestrator/output/element.js"; 32 */ 33 34const emptyFacetsList = () => 35 html` 36 <p> 37 <span> 38 You haven't saved anything yet. Add a facet by browsing the <a 39 href="featured/" 40 >featured ones</a> or any of the other categories. You can click the toggle 41 to quickly add or remove from your collection. Alternatively, add one using 42 an URI: 43 </span> 44 </p> 45 `; 46 47//////////////////////////////////////////// 48// LIST 49//////////////////////////////////////////// 50 51/** @type {() => void | undefined} */ 52let stopMonitor; 53 54/** */ 55export async function renderList() { 56 if (stopMonitor) stopMonitor(); 57 activeFilter.set((() => { 58 const stored = localStorage.getItem(FILTER_STORAGE_KEY); 59 return stored === "prelude" || stored === "interface" || stored === "base" 60 ? stored 61 : "all"; 62 })()); 63 64 /** @type {HTMLElement | null} */ 65 const listEl = document.querySelector("#list"); 66 if (!listEl) throw new Error("List element not found"); 67 68 if (listEl.getAttribute("data-rendered") === "f") { 69 listEl.innerHTML = ""; 70 listEl.removeAttribute("data-rendered"); 71 } 72 73 const out = await output(); 74 75 stopMonitor = effect(() => { 76 _renderList(out, listEl); 77 }); 78} 79 80/** 81 * @param {OutputOrchestrator} output 82 * @param {HTMLElement} listEl 83 */ 84function _renderList(output, listEl) { 85 const facetsCol = output.facets.collection(); 86 87 if (facetsCol.state !== "loaded") { 88 const loading = html` 89 <div class="with-icon" style="font-size: var(--fs-sm)"> 90 <i class="ph-bold ph-spinner animate-spin"></i> 91 Loading your software 92 </div> 93 `; 94 95 render(loading, listEl); 96 return; 97 } 98 99 const filter = activeFilter.get(); 100 101 const col = facetsCol.state === "loaded" 102 ? [...facetsCol.data] 103 .filter((c) => 104 filter === "base" ? !!c.tags?.includes("base") : (filter === "all" || 105 (filter === "prelude" 106 ? c.kind === "prelude" 107 : c.kind !== "prelude")) && 108 !c.tags?.includes("base") 109 ) 110 .sort((a, b) => { 111 return a.name.toLocaleLowerCase().localeCompare( 112 b.name.toLocaleLowerCase(), 113 ) || a.id.localeCompare(b.id); 114 }) 115 : []; 116 117 const selected = output.selected(); 118 const outputLabel = selected?.label ?? selected?.getAttribute?.("label") ?? 119 "Local storage"; 120 121 const filterBar = html` 122 <div class="grid-filter"> 123 <span class="grid-filter--label">Filter by</span> 124 <button 125 class="button--border button--tiny ${filter === "all" 126 ? "" 127 : "button--transparent"}" 128 @click="${() => activeFilter.set("all")}" 129 > 130 All 131 </button> 132 <button 133 class="button--border button--tiny button--bg-twist-4 button--tr-twist-4 ${filter === 134 "prelude" 135 ? "" 136 : "button--transparent"}" 137 @click="${() => activeFilter.set("prelude")}" 138 > 139 Features 140 </button> 141 <button 142 class="button--border button--tiny button--bg-twist-2 button--tr-twist-2 ${filter === 143 "interface" 144 ? "" 145 : "button--transparent"}" 146 @click="${() => activeFilter.set("interface")}" 147 > 148 Interfaces 149 </button> 150 151 <button 152 class="button--border button--tiny ${filter === "base" 153 ? "" 154 : "button--transparent"}" 155 title="Show the hidden essential features" 156 @click="${() => activeFilter.set("base")}" 157 > 158 Base 159 </button> 160 161 <span class="divider"></span> 162 163 <button 164 class="button--border button--tiny button--bg-accent button--tr-accent button--transparent" 165 @click="${() => openAddFromURIModal()}" 166 > 167 <span class="with-repositioned-icon"> 168 <i class="ph-fill ph-plus-circle"></i> 169 <span class="button__supplementary-text">Add from URI</span> 170 </span> 171 </button> 172 173 <div style="flex: 1"></div> 174 175 <span class="grid-filter--label grid-filter--label-output" 176 >Userdata from</span> 177 <span class="grid-filter--output">${outputLabel}</span> 178 </div> 179 `; 180 181 const h = col.length || filter !== "all" 182 ? html` 183 ${filterBar} 184 <ul class="grid" style="margin: 0"> 185 ${col.map((c, index) => { 186 const color = FacetCategory.color(c); 187 const kind = FacetCategory.name(c); 188 189 const title = c.kind === "prelude" 190 ? html` 191 <span style="display: inline-block; padding: var(--space-3xs) 0"> 192 ${c.name} 193 </span> 194 ` 195 : html` 196 <a 197 href="l/?id=${c 198 .id}" 199 style="display: inline-block; padding: var(--space-3xs) 0" 200 > 201 ${c.name} 202 </a> 203 `; 204 205 return keyed( 206 c.id, 207 html` 208 <li 209 class="grid-item" 210 style="--grid-item-color: ${color}" 211 ?data-disabled="${!(c.enabled ?? true)}" 212 > 213 <div class="grid-item__contents"> 214 <div class="grid-item__title" style="color: ${color}"> 215 ${title} 216 </div> 217 <div class="list-description"> 218 <div> 219 ${c.description?.trim().length 220 ? unsafeHTML( 221 marked.parse(c.description, { async: false }), 222 ) 223 : nothing} 224 </div> 225 <div> 226 ${c.uri && !c.html 227 ? html` 228 <span class="with-icon"> 229 <i class="ph-fill ph-binoculars"></i> 230 <span>Tracking the original <a href="${c 231 .uri}">URI</a></span> 232 </span> 233 ` 234 : html` 235 <span class="with-icon"> 236 <i class="ph-fill ph-code-simple"></i> 237 <span>Custom code</span> 238 </span> 239 `} 240 </div> 241 </div> 242 </div> 243 244 <div class="grid-item__menu ${classMap({ 245 "grid-item__menu--active": c.enabled ?? true, 246 })}"> 247 <button 248 class="button--transparent" 249 title="${(c.enabled ?? true) 250 ? c.kind === "prelude" ? "Disable" : "Dim" 251 : c.kind === "prelude" 252 ? "Enable" 253 : "Light"}" 254 @click="${toggleFacetEnabled({ id: c.id })}" 255 > 256 <i class="${(c.enabled ?? true) 257 ? c.kind === "prelude" 258 ? "ph-fill ph-lightning" 259 : "ph-fill ph-eye" 260 : c.kind === "prelude" 261 ? "ph-bold ph-lightning-slash" 262 : "ph-bold ph-eye-slash"}"></i> 263 </button> 264 <hr /> 265 <button 266 class="button--transparent" 267 title="More actions" 268 popovertarget="facet-menu-${c.id}" 269 > 270 <i class="ph-bold ph-dots-three-vertical"></i> 271 </button> 272 <div id="facet-menu-${c.id}" class="dropdown" popover> 273 <a 274 class="with-icon" 275 href="code/?id=${encodeURIComponent(c.id)}" 276 > 277 <i class="ph-fill ph-code-block"></i> 278 Edit 279 </a> 280 <a 281 class="with-icon" 282 href="#" 283 @click="${(/** @type {MouseEvent} */ e) => { 284 e.preventDefault(); 285 deleteFacet({ id: c.id })(); 286 }}" 287 > 288 <i class="ph-fill ph-skull"></i> 289 Delete 290 </a> 291 </div> 292 </div> 293 </li> 294 `, 295 ); 296 })} 297 </ul> 298 ` 299 : html` 300 ${filterBar} ${emptyFacetsList()} 301 `; 302 303 render(h, listEl); 304 305 setTimeout(() => { 306 /** @type {HTMLElement | null} */ 307 const l = listEl.querySelector(".grid-filter--label-output"); 308 309 /** @type {HTMLElement | null} */ 310 const o = listEl.querySelector(".grid-filter--output"); 311 312 if (o && l) { 313 l.style.opacity = "0.4"; 314 o.style.opacity = "1"; 315 } 316 }, 250); 317}