A music player that connects to your cloud/distributed storage.
1import * as Build from "./build.js";
2import * as Dashboard from "./dashboard.js";
3import * as Grid from "./grid.js";
4import * as Guide from "./guide.js";
5import * as Nav from "./nav.js";
6
7/** Base pathname of the app (e.g. "/" at root, "/diffuse/" in a subdirectory). */
8const BASE_PATHNAME = new URL(document.baseURI).pathname;
9
10/**
11 * Strips the app's base path prefix from an absolute pathname,
12 * returning a root-relative path like "/build".
13 *
14 * @param {string} pathname
15 */
16function relativePathname(pathname) {
17 const stripped = pathname.replace(/\/$/, "");
18 const base = BASE_PATHNAME.replace(/\/$/, "");
19 return base.length > 0 && stripped.startsWith(base)
20 ? stripped.slice(base.length)
21 : stripped;
22}
23
24/**
25 * @param {URL} url
26 */
27async function initJsBasedOnPage(url) {
28 const path = relativePathname(url.pathname);
29
30 Nav.update();
31 Nav.watchResize();
32
33 Grid.setupFilter();
34 Grid.insertToggleButtons();
35 await Grid.monitorToggleButtonStates();
36 await Grid.setupOutputIndicator();
37
38 switch (path) {
39 case "/build":
40 Build.renderEditor();
41 Build.handleBuildFormSubmit();
42 Build.listenForExamplesEdit();
43 await Build.editFacetFromURL();
44 break;
45 case "/dashboard":
46 await Dashboard.renderList();
47 break;
48 case "/guide":
49 Guide.setupSampleButton();
50 break;
51 default:
52 break;
53 }
54}
55
56initJsBasedOnPage(new URL(location.href));
57
58// Partial page updates for kitchen navigation using the Navigation API.
59// Intercepts nav link clicks, fetches the new page, and swaps <main> content
60// instead of doing a full page load.
61
62if ("navigation" in globalThis) {
63 /** @type {any} */ (globalThis).navigation.addEventListener(
64 "navigate",
65 navigateHandler,
66 );
67}
68
69/** @param {any} event */
70function navigateHandler(event) {
71 if (!event.canIntercept) return;
72
73 const url = new URL(event.destination.url);
74 if (url.origin !== location.origin) return;
75
76 // Only intercept paths one level deep
77 const relative = relativePathname(url.pathname);
78 const parts = relative.split("/").filter(Boolean);
79 if (parts.length === 0) return;
80 if (parts.length > 2) return;
81
82 // Skip the loader page
83 if (parts[0] === "l") return;
84 if (parts.includes("chronicle")) return;
85
86 event.intercept({
87 scroll: "manual",
88 async handler() {
89 const navLinks = /** @type {HTMLAnchorElement[]} */ ([
90 ...document.querySelectorAll("#diffuse-nav a, #nav-overflow-menu a"),
91 ]);
92 const stripSlash = (/** @type {string} */ p) => p.replace(/^\//, "");
93 const navLink = navLinks.find(
94 (a) =>
95 stripSlash(new URL(a.href).pathname) === stripSlash(url.pathname),
96 );
97
98 const icon = navLink?.querySelector("i");
99 const originalIconClass = icon?.className;
100 let addedSpinner = /** @type {HTMLElement | undefined} */ (undefined);
101
102 const loadingTimer = navLink
103 ? setTimeout(() => {
104 if (icon) {
105 icon.className = "ph-bold ph-spinner animate-spin";
106 } else {
107 addedSpinner = document.createElement("i");
108 addedSpinner.className = "ph-bold ph-spinner animate-spin";
109 const span = navLink.querySelector("span");
110 (span ?? navLink).prepend(addedSpinner);
111 }
112 }, 250)
113 : undefined;
114
115 let html;
116
117 try {
118 const response = await fetch(url);
119 if (!response.ok) throw new Error(`${response.status}`);
120 html = await response.text();
121 } catch {
122 clearTimeout(loadingTimer);
123 if (icon && originalIconClass !== undefined) icon.className = originalIconClass;
124 addedSpinner?.remove();
125 location.href = url.href;
126 return;
127 } finally {
128 clearTimeout(loadingTimer);
129 if (icon && originalIconClass !== undefined) icon.className = originalIconClass;
130 addedSpinner?.remove();
131 }
132
133 const parser = new DOMParser();
134 const doc = parser.parseFromString(html, "text/html");
135
136 const newMain = doc.querySelector("main");
137 const currentMain = document.querySelector("main");
138
139 if (!newMain || !currentMain) {
140 location.href = url.href;
141 return;
142 }
143
144 document.title = doc.title;
145
146 // Replace <main> content
147 const range = document.createRange();
148 range.selectNode(currentMain);
149 const documentFragment = range.createContextualFragment(
150 newMain.innerHTML ?? "",
151 );
152
153 currentMain.innerHTML = "";
154 currentMain.append(documentFragment);
155
156 initJsBasedOnPage(url);
157
158 window.scrollTo({ top: 0, behavior: "instant" });
159 },
160 });
161}