forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
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}