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 257 lines 8.2 kB view raw
1import * as Output from "~/common/output.js"; 2import { facetFromURI } from "~/common/facets/utils.js"; 3import { effect } from "~/common/signal.js"; 4 5import { output } from "./output.js"; 6 7//////////////////////////////////////////// 8// FILTER 9//////////////////////////////////////////// 10 11export function setupFilter() { 12 /** @type {NodeListOf<HTMLElement>} */ 13 const kindButtons = document.querySelectorAll( 14 ".grid-filter button[data-filter]", 15 ); 16 17 /** @type {NodeListOf<HTMLElement>} */ 18 const items = document.querySelectorAll(".grid-item"); 19 20 // Build category buttons from the categories present in the current grid 21 const categoriesEl = document.querySelector(".grid-filter--categories"); 22 const categories = /** @type {string[]} */ ( 23 [...new Set([...items].map((i) => i.dataset.category).filter(Boolean))] 24 .sort() 25 ); 26 27 /** @type {HTMLElement | null} */ 28 let categoryLabelEl = null; 29 /** @type {HTMLElement | null} */ 30 let categoryMenuEl = null; 31 32 if (categoriesEl && categories.length > 1) { 33 categoryLabelEl = document.createElement("span"); 34 categoryLabelEl.textContent = "All"; 35 36 const triggerBtn = document.createElement("button"); 37 triggerBtn.className = "button--border button--tiny button--transparent"; 38 triggerBtn.setAttribute("popovertarget", "grid-category-menu"); 39 const span = document.createElement("span"); 40 span.className = "with-icon"; 41 span.appendChild(categoryLabelEl); 42 const caret = document.createElement("i"); 43 caret.className = "ph-bold ph-caret-down"; 44 span.appendChild(caret); 45 triggerBtn.appendChild(span); 46 47 categoryMenuEl = document.createElement("div"); 48 categoryMenuEl.id = "grid-category-menu"; 49 categoryMenuEl.className = "dropdown"; 50 categoryMenuEl.setAttribute("popover", ""); 51 52 for (const cat of ["all", ...categories]) { 53 const item = document.createElement("button"); 54 item.dataset.category = cat; 55 item.textContent = cat === "all" ? "All" : cat; 56 item.addEventListener("click", () => { 57 activeCategory = cat; 58 const url = new URL(location.href); 59 if (cat === "all") url.searchParams.delete("category"); 60 else url.searchParams.set("category", cat); 61 history.replaceState(null, "", url); 62 categoryMenuEl?.hidePopover(); 63 applyFilter(activeKind, activeCategory); 64 }); 65 categoryMenuEl.appendChild(item); 66 } 67 68 categoriesEl.appendChild(triggerBtn); 69 categoriesEl.appendChild(categoryMenuEl); 70 } 71 72 const FILTER_KIND_STORAGE_KEY = "diffuse/dashboard/filter"; 73 74 let activeKind = "all"; 75 let activeCategory = "all"; 76 77 /** 78 * @param {string} kind 79 * @param {string} category 80 */ 81 function applyFilter(kind, category) { 82 kindButtons.forEach((b) => { 83 const transparent = b.dataset.filter !== kind; 84 if (b.classList.contains("button--transparent") !== transparent) { 85 b.classList.toggle("button--transparent", transparent); 86 } 87 }); 88 if (categoryLabelEl) { 89 categoryLabelEl.textContent = category === "all" ? "All" : category; 90 } 91 items.forEach((item) => { 92 const isBase = (item.dataset.tags ?? "").split(",").includes("base"); 93 if (kind === "base") { 94 item.hidden = !isBase; 95 } else { 96 const kindMatch = kind === "all" || item.dataset.kind === kind; 97 const catMatch = category === "all" || 98 item.dataset.category === category; 99 item.hidden = !(kindMatch && catMatch && !isBase); 100 } 101 }); 102 } 103 104 kindButtons.forEach((b) => { 105 b.addEventListener("click", () => { 106 activeKind = b.dataset.filter ?? "all"; 107 localStorage.setItem(FILTER_KIND_STORAGE_KEY, activeKind); 108 applyFilter(activeKind, activeCategory); 109 }); 110 }); 111 112 const storedKind = localStorage.getItem(FILTER_KIND_STORAGE_KEY); 113 activeKind = storedKind === "prelude" || storedKind === "interface" || 114 storedKind === "base" 115 ? storedKind 116 : "all"; 117 activeCategory = new URL(location.href).searchParams.get("category") ?? "all"; 118 applyFilter(activeKind, activeCategory); 119} 120 121//////////////////////////////////////////// 122// TOGGLE BUTTONS 123//////////////////////////////////////////// 124 125export function insertToggleButtons() { 126 const gridItems = /** @type {NodeListOf<HTMLLIElement>} */ ( 127 document.querySelectorAll(".grid li") 128 ); 129 130 for (const li of gridItems) { 131 const button = li.querySelector("button[data-action='toggle']"); 132 if (!button) continue; 133 134 button.addEventListener("click", async () => { 135 const uri = li.getAttribute("data-uri"); 136 const name = li.getAttribute("data-name"); 137 const kind = li.getAttribute("data-kind") ?? undefined; 138 const description = li.getAttribute("data-description") ?? undefined; 139 const tagsRaw = li.getAttribute("data-tags"); 140 const tags = tagsRaw ? tagsRaw.split(",").filter(Boolean) : undefined; 141 142 if (!uri || !name) return; 143 144 const out = await output(); 145 const collection = await Output.data(out.facets); 146 const isActive = collection.some((f) => 147 f.uri === uri && f.html === undefined 148 ); 149 150 if (isActive) { 151 out.facets.save(collection.filter((f) => f.uri !== uri)); 152 } else { 153 const facet = await facetFromURI( 154 { description, kind, name, tags, uri }, 155 { 156 fetchHTML: false, 157 }, 158 ); 159 out.facets.save([...collection, facet]); 160 } 161 }); 162 } 163} 164 165//////////////////////////////////////////// 166// SYNC ACTIVE STATES 167//////////////////////////////////////////// 168 169/** @type {() => void | undefined} */ 170let stopMonitor; 171 172export async function monitorToggleButtonStates() { 173 if (stopMonitor) stopMonitor(); 174 const out = await output(); 175 176 stopMonitor = effect(() => { 177 const gridItems = /** @type {NodeListOf<HTMLLIElement>} */ ( 178 document.querySelectorAll(".grid li") 179 ); 180 181 const col = out.facets.collection(); 182 const collection = col.state === "loaded" ? col.data : []; 183 const colMap = new Map(collection.map((f) => [f.uri, f])); 184 185 for (const li of gridItems) { 186 const uri = li.getAttribute("data-uri"); 187 const menu = /** @type {HTMLElement | null} */ ( 188 li.querySelector(".grid-item__menu") 189 ); 190 191 const button = /** @type {HTMLElement | null} */ ( 192 li.querySelector("button[data-action='toggle']") 193 ); 194 195 const icon = button?.querySelector("i"); 196 197 if (!menu || !button || !icon || !uri) continue; 198 199 const item = colMap.get(uri); 200 const isActive = item && item.html === undefined; 201 const isPrelude = li.dataset.kind === "prelude"; 202 203 menu.classList.toggle("grid-item__menu--active", isActive ?? false); 204 205 button.style.opacity = "revert-layer"; 206 button.title = isActive 207 ? (isPrelude ? "Remove feature" : "Unpin interface") 208 : (isPrelude ? "Add feature" : "Pin interface"); 209 210 icon.className = isActive 211 ? isPrelude ? "ph-bold ph-check" : "ph-fill ph-push-pin" 212 : isPrelude 213 ? "ph-bold ph-plus" 214 : "ph-bold ph-push-pin"; 215 216 /** @type {HTMLElement} */ (icon).style.transform = isActive && !isPrelude 217 ? "rotate(-45deg)" 218 : ""; 219 } 220 }); 221} 222 223//////////////////////////////////////////// 224// OUTPUT INDICATOR 225//////////////////////////////////////////// 226 227/** @type {() => void | undefined} */ 228let stopOutputIndicator; 229 230export async function setupOutputIndicator() { 231 if (stopOutputIndicator) stopOutputIndicator(); 232 233 const filterEl = document.querySelector(".grid-filter"); 234 if (!filterEl) return; 235 236 const out = await output(); 237 238 /** @type {HTMLElement | null} */ 239 const indicator = filterEl.querySelector(".grid-filter--output"); 240 if (!indicator) return; 241 242 /** @type {HTMLElement | null} */ 243 const label = filterEl.querySelector(".grid-filter--label-output"); 244 if (!label) return; 245 246 setTimeout(() => { 247 indicator.style.opacity = "1"; 248 label.style.opacity = "0.4"; 249 }, 250); 250 251 stopOutputIndicator = effect(() => { 252 const selected = out.selected(); 253 const label = selected?.label ?? selected?.getAttribute?.("label") ?? 254 "Local storage"; 255 indicator.textContent = label; 256 }); 257}